这一篇将经过一个简单的web
项目实现基于Session
的认证受权方式,也是以往传统项目的作法。
先来复习一下流程html
用户认证经过之后,在服务端生成用户相关的数据保存在当前会话
(Session)
中,发给客户端的数据将经过session_id
存放在cookie
中。在后续的请求操做中,客户端将带上session_id
,服务端就能够验证是否存在了,并可拿到其中的数据校验其合法性。当用户退出系统或session_id
到期时,服务端则会销毁session_id
。具体可查看上篇的基本概念了解。java
本案例为了方便,直接使用springboot
快速建立一个web
工程web
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>simple-mvc</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> </project>
实现认证功能,咱们通常须要这样几个资源spring
认证页面
也就是咱们常说的登陆页数据库
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Login</title> </head> <body> <form th:action="@{/login}" method="post"> <div><label> User Name : <input type="text" name="username"/> </label></div> <div><label> Password: <input type="password" name="password"/> </label></div> <div><input type="submit" value="登陆"/></div> </form> </body> </html>
页面控制器
如今有了认证页面,那我若是才能够进入到认证页面呢,同时我点击登录后,下一步该作什么呢?apache
@Controller public class LoginController { // 认证逻辑处理 @Autowired private AuthenticationService authenticationService; // 根路径直接跳转至认证页面 @RequestMapping("/") public String loginUrl() { return "/login"; } // 认证请求 @RequestMapping("/login") @ResponseBody public String login(HttpServletRequest request) { AuthenticationRequest authenticationRequest = new AuthenticationRequest(request); User user = authenticationService.authentication(authenticationRequest); return user.getUsername() + "你好!"; } }
经过客户端传递来的参数进行处理安全
public class AuthenticationRequest { private String username; private String password; public AuthenticationRequest(HttpServletRequest request){ username = request.getParameter("username"); password = request.getParameter("password"); } // 省略 setter getter }
同时咱们还须要一个状态用户信息的对象Userspringboot
public class User { private Integer userId; private String username; private String password; private boolean enable; public User(Integer userId, String username, String password, boolean enable) { this.userId = userId; this.username = username; this.password = password; this.enable = enable; } // 省略 setter getter }
有了用户了,有了入口了,接下来就是对这些数据的处理,看是否如何认证条件了cookie
@Service public class AuthenticationService{ // 模拟数据库中保存的两个用户 private static final Map<String, User> userMap = new HashMap<String, User>() {{ put("admin", new User(1, "admin", "admin", true)); put("spring", new User(2, "spring", "spring", false)); }}; private User loginByUserName(String userName) { return userMap.get(userName); } @Override public User authentication(AuthenticationRequest authenticationRequest) { if (authenticationRequest == null || StringUtils.isEmpty(authenticationRequest.getUsername()) || StringUtils.isEmpty(authenticationRequest.getPassword())) { throw new RuntimeException("帐号或密码为空"); } User user = loginByUserName(authenticationRequest.getUsername()); if (user == null) { throw new RuntimeException("用户不存在"); } if(!authenticationRequest.getPassword().equals(user.getPassword())){ throw new RuntimeException("密码错误"); } if (!user.isEnable()){ throw new RuntimeException("该帐户已被禁用"); } return user; } }
这里咱们模拟了两个用户,一个是正常使用的帐号,还有个帐号由于某些特殊的缘由被封禁了,咱们一块儿来测试一下。session
启动项目在客户端输入localhost:8080
会直接跳转到认证页面
咱们分别尝试不一样的帐户密码登陆看具体显示什么信息。
一、数据的密码不正确
二、帐户被禁用
三、数据正确的用户名和密码
此时咱们的测试均已符合预期,可以将正确的信息反馈给用户。这也是最基础的认证功能,用户可以经过系统的认证,说明他是该系统的合法用户,可是用户在后续的访问过程当中,咱们须要知道究竟是哪一个用户在操做呢,这时咱们就须要引入到会话的功能呢。
会话是指一个终端用户与交互系统进行通信的过程,好比从输入帐户密码进入操做系统到退出操做系统就是一个会话过程。
一、增长会话的控制
关于session
的操做,可参考HttpServletRqeust
的相关API
前面引言中咱们提到了session_id的概念,与客户端的交互。
定义一个常量做为存放用户信息的key,同时在登陆成功后保存用户信息
privata finl static String USER_SESSION_KEY = "user_session_key"; @RequestMapping("/login") @ResponseBody public String login(HttpServletRequest request) { AuthenticationRequest authenticationRequest = new AuthenticationRequest(request); User user = authenticationService.authentication(authenticationRequest); request.getSession().setAttribute(USER_SESSION_KEY,user); return user.getUsername() + "你好!"; }
二、测试会话的效果
既然说用户认证后,咱们将用户的信息保存在了服务端中,那咱们就测试一下经过会话,服务端是否知道后续的操做是哪一个用户呢?咱们添加一个获取用户信息的接口 /getUser
,看是否能后查询到当前登陆的用户信息
@ResponseBody @RequestMapping("/getUser") public String getUser(HttpServletRequest request){ Object object = request.getSession().getAttribute("user_"); if (object != null){ User user = (User) object; return "当前访问用户为:" + user.getUsername(); } return "匿名用户访问"; }
咱们经过客户端传递的信息,在服务端查询是否有用户信息,若是没有则是匿名用户的访问,若是有则返回该用户信息。
首先在不登陆下直接访问localhost:8080/getUser
返回匿名用户访问
登录后再访问返回当前访问用户为:admin
此时咱们已经能够看到当认证经过后,后续的访问服务端经过会话机制将知道当前访问的用户是说,这将便于咱们进一步处理对用户和资源的控制。
既然咱们知道了是谁在访问用户,接下来咱们将对用户访问的资源进行控制。
一、实现匿名用户不可访问。
前面咱们已经能够经过/getUser
的接口示例中知道是不是匿名用户,那接下来咱们就对匿名用户进行拦截后跳转到认证页面。
public class NoAuthenticationInterceptor extends HandlerInterceptorAdapter { private final static String USER_SESSION_KEY = "user_session_key"; // 前置拦截,在接口访问前处理 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object attribute = request.getSession().getAttribute(USER_SESSION_KEY); if (attribute == null){ // 匿名访问 跳转到根路径下的login.html response.sendRedirect("/"); return false; } return true; } }
而后再将自定义的匿名用户拦截器,放入到web
容器中使其生效
@Configuration public class WebSecurityConfig implements WebMvcConfigurer { // 添加自定义拦截器,保护路径/protect 下的全部接口资源 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new NoAuthenticationInterceptor()).addPathPatterns("/protect/**"); } }
咱们保护/protect
下的全部接口资源,当匿名用户访问上述接口时,都将被系统跳转到认证页面进行认证后才能够访问。
@ResponseBody @RequestMapping("/protect/getResource") public String protectResource(HttpServletRequest request){ return "这是非匿名用户访问的资源"; }
这里咱们就不尽兴测试页面的展现了。
二、根据用户拥有的权限对资源进行操做(资源查询/资源更新)
根据匿名用户处理的方式,咱们此时也可设置拦截器,对接口的权限和用户的权限进行对比,经过后放行,不经过则提示。此时咱们须要配置这样几个地方
改造用户信息,使其具备相应的权限
public class User { private Integer userId; private String username; private String password; private boolean enable; // 授予权限 private Set<String> authorities; public User(Integer userId, String username, String password, boolean enable,Set<String> authorities) { this.userId = userId; this.username = username; this.password = password; this.enable = enable; this.authorities = authorities; } }
从新设置用户
private static final Map<String, User> userMap = new HashMap<String, User>() {{ Set<String> all =new HashSet<>(); all.add("read"); all.add("update"); Set<String> read = new HashSet<>(); read.add("read"); put("admin", new User(1, "admin", "admin", true,all)); put("spring", new User(2, "spring", "spring", false,read)); }};
咱们将admin
用户设置最高权限,具备read
和update
操做,spring
用户只具备read
权限
权限拦截器
public class AuthenticationInterceptor extends HandlerInterceptorAdapter { private final static String USER_SESSION_KEY = "user_session_key"; // 前置拦截,在接口访问前处理 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object attribute = request.getSession().getAttribute(USER_SESSION_KEY); if (attribute == null) { writeContent(response,"匿名用户不可访问"); return false; } else { User user = ((User) attribute); String requestURI = request.getRequestURI(); if (user.getAuthorities().contains("read") && requestURI.contains("read")) { return true; } if (user.getAuthorities().contains("update") && requestURI.contains("update")) { return true; } writeContent(response,"权限不足"); return false; } } //响应输出 private void writeContent(HttpServletResponse response, String msg) throws IOException { response.setContentType("text/html;charset=utf‐8"); PrintWriter writer = response.getWriter(); writer.print(msg); writer.close(); response.resetBuffer(); } }
在分别设置两个操做资源的接口
@ResponseBody @RequestMapping("/protect/update") public String protectUpdate(HttpServletRequest request){ return "您正在更新资源信息"; } @ResponseBody @RequestMapping("/protect/read") public String protectRead(HttpServletRequest request){ return "您正在获取资源信息"; }
启用自定义拦截器
@Configuration public class WebSecurityConfig implements WebMvcConfigurer { // 添加自定义拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new NoAuthenticationInterceptor()).addPathPatterns("/protect/**"); registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/protect/**"); } }
此时咱们就可使用不一样的用户进行认证后访问不一样的资源来进行测试了。
固然,这仅仅是最简单的实践,特别是权限处理这一块,不少都是采起硬编码的方式处理,旨在梳理流程相关信息。而在正式的生产环境中,咱们将会采起更安全更灵活更容易扩展的方式处理,同时也会使用很是实用的安全框架进行企业级认证受权的处理,例如spring security
,shiro
等安全框架,在接下来的篇幅中,咱们将进入到sping security
的学习。加油。
(完)