上一节,我们使用自定义认证令牌类,来支持多种登陆方式,但是还是基于 Session。这一节,我们需要结合 JWT 去做认证功能。
需求
登陆时,返回token。后续接口持以 token 进行访问。
- 登陆成功后,返回 token。
- 发送请求时,验证 token。
解决方案分析
之前,我们都是基于 Session 的。所以我首先需要把 Session 关闭掉。
然后,在登陆成功处理器中,生成 token 返回。
在发送业务请求时,使用拦截器验证该 token 是否合法。合法时,设置到 SecurityContextHoler
中即可。
实现
登陆成功返回token
-
引入 jwt 包
JDK 11 中,移除了一部分 jwt 包中,需要的类。这里重新引入。
<!-- 以下均为解决 Jwt工具中 java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter --> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency> <!-- 以上均为解决 java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
也可以使用以下方式引入较新的包,有一些api与旧版不一样,需要注意。
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --> <version>0.11.2</version> <scope>runtime</scope> </dependency>
-
修改认证成功处理方法
filter.setAuthenticationSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal(); userDetails.setPassword(null); String jwt = Jwts.builder() .claim("userId", userDetails.getId()) //用户角色 .setSubject(userDetails.getUsername()) //主题 //过期时间 .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) .signWith(SignatureAlgorithm.HS512, "damai") //加密算法 密匙 .compact(); // userDetails 添加 token 字段 userDetails.setToken(jwt); HashMap<String, Object> result = new HashMap<>(); result.put("code","000000"); result.put("msg","登陆成功"); result.put("data",userDetails); String s = new ObjectMapper().writeValueAsString(result); out.write(s); out.flush(); out.close(); });
发送业务请求,验证 token
-
添加 过滤器
@Component @Slf4j public class JwtTokenFilter extends OncePerRequestFilter { @Autowired MyUserDetailServiceImpl myUserDetailService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 如果没有 token 直接放行,Spring Security会因为其没有进行认证,抛出异常 String token = request.getHeader(JwtUtil.TOKEN_HEADER); if (StringUtils.isBlank(token)) { filterChain.doFilter(request,response); return; } log.info("jwtToken:{}",token); // 解析 token Claims claims = Jwts.parser().setSigningKey(JwtUtil.SECRET).parseClaimsJws(token.replace(JwtUtil.TOKEN_HEAD, "")).getBody(); String username = claims.getSubject(); // 加载用户信息 UserDetails userDetails = myUserDetailService.loadUserByUsername(username); MyAuthenticationToken myAuthenticationToken = new MyAuthenticationToken(userDetails, null, userDetails.getAuthorities()); // 设置到 Security 上下文 SecurityContextHolder.getContext().setAuthentication(myAuthenticationToken); filterChain.doFilter(request,response); } }
-
修改 Security 配置
@Override protected void configure(HttpSecurity http) throws Exception { // 自定义未登陆时抛出的异常处理 http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); HashMap<String, Object> result = new HashMap<>(); result.put("code","111112"); result.put("msg",authException.getMessage()); out.write(new ObjectMapper().writeValueAsString(result)); out.flush(); out.close(); }); http.authorizeRequests() .anyRequest() .authenticated() .and() .csrf() .disable(); // 关闭 session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 添加 jwt过滤器,注意一定要在UsernamePasswordAuthenticationFilter之前 http.addFilterBefore(jwtTokenFilter,UsernamePasswordAuthenticationFilter.class); http.addFilterAt(myAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); }
登陆
-
登陆返回Token
{ "msg": "登陆成功", "code": "000000", "data": { "id": 1, "phone": null, "username": "张三", …… "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjEsInN1YiI6IuW8oOS4iSIsImV4cCI6MTYwNTQ5OTk0Nn0.ZlPGSpyOLaKSeGSdDhuyuLDASJsya1rP2cVdy-JBVKVIBijC6DmOlNRnQzhvFcofWFWqeyhHpGsio1g6HmBHYw", "authorities": [ { "authority": "admin" } ], } }
-
直接访问业务接口
请求头中,不携带 token。
{ "msg": "Full authentication is required to access this resource", "code": "111112" }
-
携带 token,请求业务接口
{ "_links": { "self": { "href": "http://localhost:3344/actuator", "templated": false }, "health": { "href": "http://localhost:3344/actuator/health", "templated": false }, "health-path": { "href": "http://localhost:3344/actuator/health/{*path}", "templated": true }, "info": { "href": "http://localhost:3344/actuator/info", "templated": false } } }