『Spring Security』(七) 使用JWT认证方式

1,104 阅读1分钟

上一节,我们使用自定义认证令牌类,来支持多种登陆方式,但是还是基于 Session。这一节,我们需要结合 JWT 去做认证功能。

需求

登陆时,返回token。后续接口持以 token 进行访问。

  1. 登陆成功后,返回 token。
  2. 发送请求时,验证 token。

解决方案分析

之前,我们都是基于 Session 的。所以我首先需要把 Session 关闭掉。

然后,在登陆成功处理器中,生成 token 返回。

在发送业务请求时,使用拦截器验证该 token 是否合法。合法时,设置到 SecurityContextHoler中即可。

实现

登陆成功返回token

  1. 引入 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>
    
  2. 修改认证成功处理方法

    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

  1. 添加 过滤器

    @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);
        }
    }
    
  2. 修改 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);
        }
    

登陆

  1. 登陆返回Token

    {
        "msg": "登陆成功",
        "code": "000000",
        "data": {
            "id": 1,
            "phone": null,
            "username": "张三",
             ……
            "token": "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjEsInN1YiI6IuW8oOS4iSIsImV4cCI6MTYwNTQ5OTk0Nn0.ZlPGSpyOLaKSeGSdDhuyuLDASJsya1rP2cVdy-JBVKVIBijC6DmOlNRnQzhvFcofWFWqeyhHpGsio1g6HmBHYw",
            "authorities": [
                {
                    "authority": "admin"
                }
            ],
        }
    }
    
  2. 直接访问业务接口

    请求头中,不携带 token。

    {
        "msg": "Full authentication is required to access this resource",
        "code": "111112"
    }
    
  3. 携带 token,请求业务接口

    image-20201116124158212
    {
        "_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
            }
        }
    }