整合篇4:Spring Security+JWT进行Token安全校验

1,441 阅读2分钟

1. pom依赖引入

<!-- Hutool:Java工具类库 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.15</version>
</dependency>
<!-- jjwt:JSON Web令牌工具 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!-- spring-boot-starter-security:鉴权认证框架 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.6.2</version>
</dependency>

2. 检验参数

#自定义token配置参数
token:
  tokenHeader: Authorization #JWT存储的请求头
  tokenHead: Bearer #JWT负载中拿到开头
  salt: T-zhigong #JWT加盐处理
  expirationTime: 10080 #JWT到期时间
  signer: T-zhigong
  redis_token_key: HE_TOKEN #存储token的redis路由键

3. JWTToken工具类编写

package com.zhigong.heavenearth.utils;

import cn.hutool.core.util.IdUtil;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.util.Date;
import java.util.HashMap;

/**
* @author 田治功 
* @description JWT令牌生成工具类
* @date 2022-01-06 19:02
*/
@Component
public class JWTTokenUtil {
    
    //加密盐
    @Value("${token.salt}")
    private String salt;
    
    //签发人
    @Value("${token.signer}")
    private String signer;
    
    //过期时间
    @Value("${token.expirationTime}")
    private Long expiration;
    
    
    /**
    * 生成token
    *
    * @param userDetails
    * @return
    */
    public String creatToken(UserDetails userDetails) {
        //1.生成加盐签名密钥
        byte[] bytes = DatatypeConverter.parseBase64Binary(salt);
        SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, SignatureAlgorithm.HS256.getJcaName());
        //2.生成JWT的时间
        long currentTimeMillis = System.currentTimeMillis();
        Date createDate = new Date(currentTimeMillis);
        //3.添加过期的时间
        long expiredTimeMillis = currentTimeMillis + expiration * 60 * 1000;
        Date expiredDate = new Date(expiredTimeMillis);
        //4.添加私有声明
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("username", userDetails.getUsername());
        //5.设置JWT构造器
        JwtBuilder jwtBuilder = Jwts.builder()
            //以下设置为JWT头部(header)部分,token类型和采用的加密算法(经测试加密算法自动设置无需自主设置,类型亦可不设置)
            .setHeaderParam("typ", "JWT")
            //以下设置为JWT载荷(payload)部分,标准声明(建议但不强制全部使用)
            .setClaims(claims)//设置私有声明,自定义的参数信息
            .setSubject(userDetails.getUsername())//设置jwt所面向的用户(主题,代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串)
            .setId(IdUtil.simpleUUID())//设置JWT唯一标识(根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击)
            .setIssuedAt(createDate)//设置jwt的签发时间
            .setIssuer(signer)//设置签发者
            .setAudience("IE")//设置接收jwt的一方
            //.setNotBefore()//定义在什么时间之前,该jwt都是不可用的.
            .setExpiration(expiredDate)//设置jwt的过期时间,这个过期时间必须要大于签发时间
            .signWith(SignatureAlgorithm.HS256, secretKeySpec);//设置签名使用的签名算法和签名使用的秘钥
        //6.生成JWT令牌
        return jwtBuilder.compact();
    }
    
    /**
    * 解密token
    *
    * @param token
    * @return
    */
    public Claims decryptToken(String token) {
        //1.生成加盐签名密钥
        byte[] bytes = DatatypeConverter.parseBase64Binary(salt);
        SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, SignatureAlgorithm.HS256.getJcaName());
        Claims claims;
        //2.不论token是否过期,都返回claims对象
        try {
            claims = Jwts.parser()
                .setSigningKey(secretKeySpec) //设置签名
                .parseClaimsJws(token)  //解析token
                .getBody();
        } catch (ExpiredJwtException e) {
            claims = e.getClaims();
        }
        return claims;
    }
}

4. Security配置类编写

package com.zhigong.heavenearth.config;

import com.zhigong.heavenearth.config.security.SecurityTokenAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 田治功 
 * @description Security配置类
 * @date 2022-01-06 16:18
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityTokenAuthenticationFilter securityTokenAuthenticationFilter;

    @Value("${spring.profiles.active}")
    private String env;

    /**
     * 验证请求重载
     *
     * @return
     * @throws Exception
     */
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * URL请求配置
     *
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //配置规则和参数
        httpSecurity.csrf().disable()//由于使用的是JWT,我们这里不需要csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//基于token,所以不需要session
                .authorizeRequests()//授权配置
                .antMatchers(getUrls()).permitAll()//请求过滤
                .anyRequest().authenticated();//除以上请求外的所有请求需要鉴权认证
        //添加JWT Filter
        httpSecurity.addFilterBefore(securityTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        //禁用缓存
        httpSecurity.headers().cacheControl();
        //允许跨域
        httpSecurity.headers().frameOptions().disable();
    }

    /**
     * 密码生成策略
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 请求过滤参数
     *
     * @return
     */
    private String[] getUrls() {
        List<String> list = new ArrayList<>();
        if (!env.contains("prod")) {
            list.add("/swagger-ui.html/**");
            list.add("/swagger-ui/**");
            list.add("/swagger-resources/**");
            list.add("/webjars/**");
            list.add("/v3/**");
            list.add("/v2/**");
            list.add("/*/api-docs");
            list.add("/doc.html");
            list.add("/**");
        }
        list.add("/userAccount/login");
        String[] strings = list.toArray(new String[0]);
        return strings;
    }
}

5. Security用户登录详情处理

package com.zhigong.heavenearth.config.security;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zhigong.heavenearth.mapper.UserAccountMapper;
import com.zhigong.heavenearth.dto.user.LoginDetailsDTO;
import com.zhigong.heavenearth.dto.user.UserAccountDTO;
import com.zhigong.heavenearth.pojo.UserAccount;
import com.zhigong.heavenearth.utils.JWTTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @author 田治功 
 * @description Security用户详情服务类
 * @date 2022-01-06 17:57
 */
@Service
public class SecurityUserDetailService implements UserDetailsService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private UserAccountMapper userAccountMapper;

    @Autowired
    private JWTTokenUtil jwtTokenUtil;

    //redis路由键
    @Value("${token.redis_token_key}")
    private String redisTokenKey;

    //redis过期时间(同JWT过期时间)
    @Value("${token.expirationTime}")
    private int expirationTime;

    /**
     * 实现核心用户信息处理
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public LoginDetailsDTO loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.根据用户名查询相应用户信息
        LambdaQueryWrapper<UserAccount> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserAccount::getAccount, username);
        UserAccount userAccount = userAccountMapper.selectOne(queryWrapper);
        //2.根据查询到的用户信息可进行一些策略验证,如(用户不存在,用户被逻辑删除等,视业务决定)
        if (ObjectUtil.isEmpty(userAccount)) {
            throw new UsernameNotFoundException("用户不存在");
        }
        //3.实例化用户详情类
        LoginDetailsDTO loginDetailsDTO = new LoginDetailsDTO(userAccount.getAccount(), userAccount.getPassword());
        //4.颁发token授权令牌
        String token = jwtTokenUtil.creatToken(loginDetailsDTO);
        UserAccountDTO userAccountDTO = new UserAccountDTO();
        BeanUtil.copyProperties(userAccount, userAccountDTO);
        //额:设定账户角色
//        if (userAccount.getCreation() == 0) {
//            HashSet<Object> objects = new HashSet<>();
//            objects.add(AccountIdentityCode.SYSTEM.toString());
//            loginDetailsDTO.setAuthorities(objects);
//        }
//        if (userAccount.getCreation() == 1) {
//            HashSet<Object> objects = new HashSet<>();
//            objects.add(AccountIdentityCode.PERSONAL.toString());
//            loginDetailsDTO.setAuthorities(objects);
//        }
        loginDetailsDTO.setUserAccountDTO(userAccountDTO);
        loginDetailsDTO.setToken(token);
        //5.将token信息存储到redis中
        ValueOperations<String, LoginDetailsDTO> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(redisTokenKey + ":" + token, loginDetailsDTO, expirationTime, TimeUnit.MINUTES);
        return loginDetailsDTO;
    }
}

6. Security Token拦截器

package com.zhigong.heavenearth.config.security;

import com.zhigong.heavenearth.dto.user.LoginDetailsDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 田治功 
 * @description Token拦截器实列类
 * @date 2022-01-06 16:38
 */
@Component
@Slf4j
public class SecurityTokenAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    RedisTemplate redisTemplate;

    //请求头
    @Value("${token.tokenHeader}")
    private String tokenHeader;

    //请求令牌中特定的字符序列
    @Value("${token.tokenHead}")
    private String tokenHead;

    //redis路由键
    @Value("${token.redis_token_key}")
    private String redisTokenKey;


    /**
     * token过滤器配置
     *
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //1.从请求头部获取授权令牌
        final String authHead = request.getHeader(this.tokenHeader);
        //2.验证授权令牌是否为空,不为空再验证传入授权令牌字符串中手头存在指定的前缀(特定的字符序列)
        if (authHead != null && authHead.startsWith(tokenHead)) {
            //2.0 删除请求的授权令牌中的特定字符序列
            final String authToken = authHead.substring(tokenHead.length());
            //2.1 判断redis中是否存在相应的token信息
            if (redisTemplate.hasKey(redisTokenKey + ":" + authToken)) {
                //2.1.2 从redis中取出相应的用户信息详情
                ValueOperations<String, LoginDetailsDTO> valueOperations = redisTemplate.opsForValue();
                LoginDetailsDTO loginDetailsDTO = valueOperations.get(redisTokenKey + ":" + authToken);
                //2.1.3 简单呈现用户名和密码
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDetailsDTO, null, loginDetailsDTO.getAuthorities());
                //2.1.4 从HttpServletRequest对象构建详细信息对象,并设置详情
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                //2.1.5 将给定的SecurityContext与当前执行线程相关联
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        /**
         * 将请求转发给过滤器链下一个filter
         */
        filterChain.doFilter(request, response);
    }

}

7. 改写登录服务逻辑

    /**
     * 登录服务
     *
     * @param loginParamDTO
     * @return
     */
    @ApiOperation("登录")
    @PostMapping("login")
    @LogAnnotation(OperateModule = "登录", OperateType = "登录", OperateDesc = "系统登录")
    public Result login(@RequestBody LoginParamDTO loginParamDTO) {
        LoginDetailsDTO loginDetailsDTO = securityUserDetailService.loadUserByUsername(loginParamDTO.getUserName());
        return Result.success(loginDetailsDTO);
    }

8. 请求头预览效果(以Swagger为例)