阅读本文以前,首先要了解 Spring security 的相关内容,最起码须要将我以前的系列文章看完。其次须要学习 jwt 的相关知识。关于 jwt(全称JSON Web Token) ,推荐参考阮一峰大神的 JSON Web Token 入门教程,也可翻看其余相关资料。html
业务需求:后台的一些接口须要用户登陆(好比获取用户列表:user/list)时,才有权限访问。git
业务实现:客户端访问 user/list 接口时,须要在请求头携带 token,而后后台经过判断 token 的有效性去选择放行仍是拦截请求。github
业务难点:token 的有效性如何判断?web
难点攻破:token的有效性借助 jwt,因此须要充分理解 jwt 的相关知识。spring
业务流程:当访问 user/list 接口时,后台经过一个过滤器拦截,从请求头中获取token信息,而后经过 jwt工具类校验token的有效性。json
我本身是将上一篇文章中的工程复制了一份,而后作了些改动,此次改动的东西比较多(增长和删除了一些类,增长了一些配置),固然我会尽可能讲清楚作了哪些改动,因此你们自行选择新建工程仍是复制一份。api
较之上一篇的内容,本次只新增了 jwt 的依赖包springboot
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--JWT(Json Web Token)--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
这里能够简单看一下,根据 jwt 相关知识以及看后面的代码会明白他们的意义。app
jwt: tokenHeader: Authorization # JWT存储的请求头 secret: mySecret # JWT加解密使用的密钥 expiration: 604800 # JWT的超期限时间(60*60*24) tokenHead: Bearer # JWT负载中拿到开头
只是图个本身方便,从别处 copy 的工具类,须要你们简单看下。三个类合在一块儿的做用就是一个 json 格式的响应数据的工具。代码放在 common 包下面。ide
从别处拷贝了一个 JwtTokenUtil 类,里面封装了一些方法,诸如:生成token,判断token的有效性等。不用纠结方法是如何实现的,只管拿来用(除非出现问题),方法见名知意。
在 UserController 类里面定义一个登陆方法,以下所示。
须要调用UserDetailService 的loadUserByUsername方法,若是看过我以前的文章,这里应该清楚这个方法主要作用户校验的。
调用 security 的 api, SecurityContextHolder.getContext().setAuthentication(authentication),能够理解为security认证用户登陆成功
生成 token 信息返回给客户端,这个 token 是供客户端调用其余接口(须要用户登陆才能调用的接口)时使用的。
/** * 用户登陆接口 * [@param](https://my.oschina.net/u/2303379) username 用户名 * [@param](https://my.oschina.net/u/2303379) password 密码 * [@return](https://my.oschina.net/u/556800) */ @PostMapping("/login") public Object login(@RequestParam("username") String username, @RequestParam("password") String password) { try { // 校验用户信息 UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 保存用户登陆态 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication);// 进行到这一步,security 才会认为你登录成功 // 根据用户信息生成 token String token = jwtTokenUtil.generateToken(userDetails); return CommonResult.success(token);// 到这里, 从咱们的业务角度来讲登录成功了 } catch (AuthenticationException e) { System.out.println("登陆异常: " + e.getMessage()); return CommonResult.failed("登陆失败"); }
}
过滤器的功能就是用来拦截请求的 url ,而后校验 token 的有效性。 SecurityContextHolder.getContext().setAuthentication(authentication); 这段代码着重说一下,咱们能够简单理解为 SecurityContextHolder.getContext() 为 Security 提供的一个保存已登陆用户信息的一个容器,经过调用它的 setAuthentication 以及getAuthentication方法,能够将用户信息存入容器和从容器中取出。
/** * JWT登陆受权过滤器 */ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 1. 从请求头中取出 token ,key随便定义,我这里定义在了配置文件中 String authHeader = request.getHeader(this.tokenHeader); // 2. 判断是否为空,以及是否以 "Bearer " 开头 ps :固定格式 if (authHeader != null && authHeader.startsWith(this.tokenHead)) { String authToken = authHeader.substring(this.tokenHead.length());// 必须以 "Bearer " 开头 // 3. 从 token 里获取用户名 String username = jwtTokenUtil.getUserNameFromToken(authToken); // 4. 只要 token 没过时,就让用户保持登陆状态 jwt 令牌只是辅助登陆, 真正是否登陆要看 authentication 是否有效 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { // 下面这段代码就是为了让 security 保持用户登陆状态, 在UserController 里你也能够看到 UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 核心代码 SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
在 WebSecurityConfig 配置类的configure方法中的最后一行加上以下代码。该方法中的其余代码的含义,不懂得能够参考前几篇博客。
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable();// 必须有, 否则会 403 forbidden /*http.formLogin() .loginPage("/loginPage.html")// 自定义登陆页 .loginProcessingUrl("/form/login")// 自定义登陆 action, 名字随便起 .successHandler(successHandler)// 自定义登陆成功处理类 .failureHandler(failureHandler);// 自定义登陆失败处理类*/ // 访问 "/form/login", "/loginPage.html" 放行 http.authorizeRequests().antMatchers("/user/userInfo", "/user/login", "/form/login", "/loginPage.html").permitAll() .antMatchers("/hello").hasRole("superadmin") // 只有superadmin 角色的用户才能访问 .anyRequest().authenticated(); /*http.exceptionHandling() .accessDeniedHandler(accessDeniedHandler)// 用户没有访问权限处理器 .authenticationEntryPoint(entryPoint);// 用户没有登陆处理器*/ // 这里是新增的代码 http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); }
至此,Sprng Securiy 系列博客更新完毕。这里可能须要声明一下, 个人一系列文章可能没讲 Spring Security 的工做流程,或者底层原理,全程在将如何使用 Spring Security。好比下面这段凭空而来的代码,只是讲了这段代码的逻辑含义,没有讲清楚为何这样用。这些是我之后要努力完成的方向。只是但愿帮助到须要的人,也欢迎你们给出意见和批评。
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); }