了解

JWT简介

开发

依赖配置

springboot使用最新版2.5.3

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>io.jsonwebtoken</groupId>
  7. <artifactId>jjwt</artifactId>
  8. <version>0.9.1</version>
  9. </dependency>

配置

常量配置

public interface Const {
//TOKEN
    /**
     * 令牌 header key
     */
    String TOKEN_HEADER = "Authorization";
    /**
     * 令牌前缀
     */
    String TOKEN_PREFIX = "Bearer ";
    /**
     * token 加密密钥
     */
    String TOKEN_SECRET = "SpringSecurityAndJWT";
    /**
     * token 过期时间
     */
    long TOKEN_EXPIRE_TIME = 7200;
}

用户对象

要使用 Spring Security 实现对用户的权限控制,
首先需要实现一个简单的 User 对象实现 UserDetails 接口,
UserDetails 接口负责提供核心用户的信息,
如果你只需要用户登陆的账号密码,不需要其它信息,如验证码等,那么你可以直接使用 Spring Security 默认提供的 User 类,而不需要自己实现
该用户对象是在登录成功后可以通过security的对象进行获取到的信息

@Data
public class LoginUser implements UserDetails {

    private static final long serialVersionUID = 7481749302887905270L;

    private String username;

    private String password;

    private Boolean rememberMe;

    private String captcha;

    private String token;

    private Long loginTime;

    private Long expireTime;

    private Set<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

这个就是我们要使用到的 User 对象,其中包含了【记住我,验证码】等登陆信息,
因为 Spring Security 整合 Jwt 本质上就是用自己自定义的登陆过滤器,去替换 Spring Security 原生的登陆过滤器,
这样的话,原生的记住我功能就会无法使用,所以我在 User 对象里添加了记住我的信息,用来自己实现这个功能

JWT 令牌认证工具


/**
 * @author xupu
 * @Description 处理Token认证过程中的验证和请求
 * @Date 2021-10-14 17:16
 */
public class TokenAuthentication {

    /**
     * 设置登陆成功后令牌返回
     */
    public static void addAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        // 获取用户登陆角色
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        // 遍历用户角色
        StringBuffer stringBuffer = new StringBuffer();
        authorities.forEach(authority -> {
            stringBuffer.append(authority.getAuthority()).append(",");
        });
        long expirationTime = Const.TOKEN_EXPIRE_TIME;
        int cookExpirationTime = -1;
        // 处理登陆附加信息
        LoginUser loginDetails = (LoginUser) authentication.getDetails();
        if(loginDetails.getRememberMe() != null && loginDetails.getRememberMe()) {
            expirationTime = Const.REMEMBER_ME_EXPIRE_TIME * 1000;
            cookExpirationTime = Const.REMEMBER_ME_EXPIRE_TIME;
        }

        //生成jwt
        String jwt = Jwts.builder()
                         // Subject 设置用户名
                         .setSubject(authentication.getName())
                         // 设置用户权限
                         .claim("authorities", stringBuffer)
                         // 过期时间
                         .setExpiration(new Date(System.currentTimeMillis() + expirationTime))
                         // 签名算法
                         .signWith(SignatureAlgorithm.HS512, Const.TOKEN_SECRET).compact();

        Cookie cookie = new Cookie(Const.COOKIE_TOKEN, jwt);
        cookie.setHttpOnly(true); //使用 HTTP Only 的 Cookie 可以有效防止 XSS 攻击
        cookie.setPath("/");
        cookie.setMaxAge(cookExpirationTime);
        response.addCookie(cookie);

        // 向前端写入数据
        ServletUtils.renderString(response, JSON.toJSONString(R.ok(loginDetails)));
    }

    /**
     * 对请求的验证
     * 如果用户的 JWT 解析正确,则向 Spring Security 返回 usernamePasswordAuthenticationToken
     * 用户名密码验证令牌,告诉 Spring Security 用户所拥有的权限,并放到当前的 Context中,然后执行过滤链使请求继续执行下去
     */
    public static Authentication getAuthentication(HttpServletRequest request) {

        Cookie cookie = WebUtils.getCookie(request, Const.COOKIE_TOKEN);
        String token = cookie != null ? cookie.getValue() : null;

        if(token != null) {
            Claims claims = Jwts.parser().setSigningKey(Const.TOKEN_SECRET).parseClaimsJws(token).getBody();

            // 获取用户权限
            Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get("authorities").toString().split(","))
                                                                       .map(SimpleGrantedAuthority::new)
                                                                       .collect(Collectors.toList());

            String userName = claims.getSubject();
            if(userName != null) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, null, authorities);
                usernamePasswordAuthenticationToken.setDetails(claims);
                return usernamePasswordAuthenticationToken;
            }
            return null;
        }
        return null;
    }

}

工具类方法

   /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     */
    public static void renderString(HttpServletResponse response, String string) {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = null;
        try {
            out = response.getWriter();
            response.setStatus(200);
            out.print(string);
        } catch(IOException e) {
            e.printStackTrace();
        } finally {
            assert out != null;
            out.flush();
            out.close();
        }
    }

Token过滤器

众所周知,Spring Security 是借助一系列的 Servlet Filter 来来实现提供各种安全功能的,
所以我们要使用 JWT 就需要自己实现两个和 JWT 有关的过滤器

  1. 一个是用户登录的过滤器:

在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个 token 返回给客户端,登录失败则给前端一个登录失败的提示。

  1. 第二个是其他请求过滤器:

是当其他请求发送来,校验 token 的过滤器,如果校验成功,就让请求继续执行。否则返回校验不通过的提示

security配置类

这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter 的部分 configure配置,来实现用户自定义的部分。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Bean
    public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtTokenFilter();
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
        auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() );
    }

    @Override
    protected void configure( HttpSecurity httpSecurity ) throws Exception {
        httpSecurity.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() 
                .antMatchers(HttpMethod.POST, "/authentication/**").permitAll()  
                .antMatchers(HttpMethod.POST).authenticated()
                .antMatchers(HttpMethod.PUT).authenticated()
                .antMatchers(HttpMethod.DELETE).authenticated()
                .antMatchers(HttpMethod.GET).authenticated();


        httpSecurity
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

        httpSecurity.headers().cacheControl();
    }
}

JWT工具类

主要是集成一些基础的功能,比如生成token,验证token,刷新token等功能

public class JwtTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    /**
     * 根据claims生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从token中获取JWT中的负载claims
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}", token);
        }
        return claims;
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token中获取登录用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据用户信息生成token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 当原来的token没过期时是可以刷新的
     *
     * @param oldToken 带tokenHead的token
     */
    public String refreshHeadToken(String oldToken) {
        if (StrUtil.isEmpty(oldToken)) {
            return null;
        }
        String token = oldToken.substring(tokenHead.length());
        if (StrUtil.isEmpty(token)) {
            return null;
        }
        //token校验不通过
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        //如果token已经过期,不支持刷新
        if (isTokenExpired(token)) {
            return null;
        }
        //如果token在30分钟之内刚刷新过,返回原token
        if (tokenRefreshJustBefore(token, 30 * 60)) {
            return token;
        } else {
            claims.put(CLAIM_KEY_CREATED, new Date());
            return generateToken(claims);
        }
    }

    /**
     * 判断token在指定时间内是否刚刚刷新过
     *
     * @param token 原token
     * @param time  指定时间(秒)
     */
    private boolean tokenRefreshJustBefore(String token, int time) {
        Claims claims = getClaimsFromToken(token);
        Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
        Date refreshDate = new Date();
        //刷新时间在创建时间的指定时间内
        if (refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time))) {
            return true;
        }
        return false;
    }
}

请求过滤器

对前端发起的请求进行过滤,用于对token进行处理

/**
 * JWT登录授权过滤器
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    @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 {
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("checking username:{}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                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));
                    LOGGER.info("authenticated user:{}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

业务逻辑代码

登录

 @Override
    public String login( String username, String password ) {
        UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password );
        final Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        final UserDetails userDetails = userDetailsService.loadUserByUsername( username );
        final String token = jwtTokenUtil.generateToken(userDetails);
        return token;
    }

注册

  @Override
    public User register( User userToAdd ) {
        final String username = userToAdd.getUsername();
        if( userRepository.findByUsername(username)!=null ) {
            return null;
        }
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        final String rawPassword = userToAdd.getPassword();
        userToAdd.setPassword( encoder.encode(rawPassword) );
        return userRepository.save(userToAdd);
    }

end

附录: