SpringBoot整合SpringSecurity(九)结合JWT(非OAuth2)

序言

jwt有好处也有坏处,好处就是不用在去存这些session了,省空间,做分布式会话so easy。但是我个人是比较不推荐你使用这个的。我举例一下几种缺点

  • 无法满足注销场景
  • 无法满足修改密码场景
  • 无法满足token续签场景

烦人的不能到期,控制到期设置黑名单,还是用到了存储比如redis,说实话这有点本末倒置,坏处不多说,如果不强求那么多安全,还是可以使用jwt减少成本,快速开发。

代码请参考 https://github.com/AutismSuperman/springsecurity-example

思路

只需要加入一个过滤器,保证他在认证过滤器的前面即可。这样就可以做到在认证前对 token的一个效验。

这样就ok了,理解了就开始做。

实践

切记引入

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>
</dependency>

数据

首先准备User实体

@Data
public class User implements UserDetails {

    private Long id;
    private String userName;
    private String password;
    private List<String> roles;


    public User(Long id, String userName, String password, List<String> roles) {
        this.id = id;
        this.userName = userName;
        this.password = password;
        this.roles = roles;
    }

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

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

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

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return authorities;
    }

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

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

}

然后准备初始化user用户数据

接口

public interface IUserService {
    User findByUsername(String userName);
}

然后是实现类

@Service
public class UserServiceImpl implements IUserService {

    private static final Set<User> users = new HashSet<>();
	// 密码123    md5加密 
    static {
        users.add(new User(1L, "fulin", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
        users.add(new User(1L, "xiaohan", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
        users.add(new User(1L, "longlong", "1dc568b64c0f67e7a86c89a12fa5bd5f", Arrays.asList("admin", "docker")));
    }

    @Override
    public User findByUsername(String userName) {
        return users.stream().filter(o -> StringUtils.equals(o.getUsername(), userName)).findFirst().get();
    }
}

userDetailService

@Service
public class UserService implements UserDetailsService {

    final IUserService iUserService;

    public UserService(IUserService iUserService) {
        this.iUserService = iUserService;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = iUserService.findByUsername(s);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return user;
    }

}

jwt

准备一下jwt的配置

@Data
@ConfigurationProperties(prefix = "security.jwt")
public class SecurityProperties {
    private JwtProperties jwt = new JwtProperties();
}

配置类

/**
 * Jwt的基本配置
 */
@Data
public class JwtProperties {
    /**
     * 默认前面秘钥
     */
    private String secret = "defaultSecret";

    /**
     * token默认有效期时长,1小时
     */
    private Long expiration = 3600L;
    /**
     * token默认有效期时长,1个半小时
     */
    private Long refreshExpiration = 5400L;

    /**
     * token的唯一标记
     */
    private String md5Key = "randomKey";

    /**
     * GET请求是否需要进行Authentication请求头校验,true:默认校验;false:不拦截GET请求
     */
    private boolean preventsGetMethod = true;
}

配置类别忘了开启哟

@SpringBootApplication
@EnableConfigurationProperties(SecurityProperties.class)
public class SecuritySimpleJwtApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecuritySimpleJwtApplication.class, args);
    }
}

然后我根据jjwt封装了一个util方便使用

/**
 * Jwt Util
 */
@Component
public class JwtTokenUtil {

    private final SecurityProperties securityProperties;

    public JwtTokenUtil(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    /**
     * 获取用户名从token中
     */
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token).getSubject();
    }

    /**
     * 获取jwt失效时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token).getExpiration();
    }

    /**
     * 获取私有的jwt claim
     */
    public String getPrivateClaimFromToken(String token, String key) {
        return getClaimFromToken(token).get(key).toString();
    }

    /**
     * 获取md5 key从token中
     */
    public String getMd5KeyFromToken(String token) {
        return getPrivateClaimFromToken(token, securityProperties.getJwt().getMd5Key());
    }

    /**
     * 获取jwt的payload部分
     */
    public Claims getClaimFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(generalKey())
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * <pre>
     *  验证token是否失效
     *  true:过期   false:没过期
     * </pre>
     */
    public Boolean isTokenExpired(String token) {
        try {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        } catch (ExpiredJwtException expiredJwtException) {
            return true;
        }
    }

    /**
     * 生成token(通过用户名和签名时候用的随机数)
     */
    public String generateToken(String userName, String randomKey) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(securityProperties.getJwt().getMd5Key(), randomKey);
        return doGenerateToken(claims, userName);
    }

    /**
     * 生成token(通过用户名和签名时候用的随机数)
     */
    public String generateRefreshToken(String userName, String randomKey) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(securityProperties.getJwt().getMd5Key(), randomKey);
        return doGenerateRefreshToken(claims, userName);
    }

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public SecretKey generalKey() {
        byte[] encodedKey = Base64.decodeBase64(securityProperties.getJwt().getSecret());
        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    }

    /**
     * 生成token
     */
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        final Date createdDate = new Date();
        final Date expirationDate = new Date(createdDate.getTime() + securityProperties.getJwt().getExpiration() * 1000);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, generalKey())
                .compact();
    }

    /**
     * 生成token
     */
    private String doGenerateRefreshToken(Map<String, Object> claims, String subject) {
        final Date createdDate = new Date();
        final Date expirationDate = new Date(createdDate.getTime() + securityProperties.getJwt().getRefreshExpiration() * 1000);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, generalKey())
                .compact();
    }

    /**
     * 获取混淆MD5签名用的随机字符串
     */
    public String getRandomKey() {
        return getRandomString(6);
    }

    /**
     * 获取随机位数的字符串
     */
    public String getRandomString(int length) {
        final String base = "abcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

    /**
     * 刷新token
     *
     * @param token:token
     * @return
     */
    public String refreshToken(String token, String randomKey) {
        String refreshedToken;
        try {
            final Claims claims = getClaimFromToken(token);
            refreshedToken = generateToken(claims.getSubject(), randomKey);
        } catch (Exception e1) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

}

颁发token

都准备齐全了,颁发token呢我们就放在成功处理器去做,这里为了让jwt具有刷新功能,特意准备了一个具备刷新功能的 token refreshToken

@Slf4j
@Component
public class AppAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {


    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        final String randomKey = jwtTokenUtil.getRandomKey();
        String username = ((UserDetails) authentication.getPrincipal()).getUsername();
        log.info("username:{}", username);
        //生产JWT 令牌
        final String token = jwtTokenUtil.generateToken(username, randomKey);
        final String refreshToken = jwtTokenUtil.generateRefreshToken(username, randomKey);
        log.info("登录成功!");
        ModelMap modelMap = GenerateModelMap.generateMap(HttpStatus.OK.value(), "登陆成功");
        modelMap.put("token", JwtAuthenticationTokenFilter.TOKEN_PREFIX + token);
        modelMap.put("refreshToken", JwtAuthenticationTokenFilter.TOKEN_PREFIX + refreshToken);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(modelMap));
    }
}

jwt过滤器

接下来就是最重要的我们自己定义一个jwt过滤器,这是我自己实现的代码如下,这里访问/refreshToken 重新颁发一个tokenrefreshToken `给前台

/**
 * JWT过滤器
 * <p>
 * OncePerRequestFilter,顾名思义,
 * 它能够确保在一次请求中只通过一次filter,而需要重复的执行。
 */
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final String HEADER_NAME = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private UserService userService;


    private AntPathMatcher antPathMatcher = new AntPathMatcher();


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        log.info("请求路径:{},请求方式为:{}", request.getRequestURI(), request.getMethod());
        if (antPathMatcher.match("/favicon.ico", request.getRequestURI())) {
            log.info("jwt不拦截此路径:{},请求方式为:{}", request.getRequestURI(), request.getMethod());
            filterChain.doFilter(request, response);
            return;
        }
        /*
         * get请求是否需要进行Authentication请求头校验,true:默认校验;false:不拦截GET请求
         * 因为get请求比较特殊
         */
        if (!securityProperties.getJwt().isPreventsGetMethod()) {
            if (Objects.equals(RequestMethod.GET.toString(), request.getMethod())) {
                log.info("jwt不拦截此路径因为开启了不拦截GET请求:{},请求方式为:{}", request.getRequestURI(), request.getMethod());
                filterChain.doFilter(request, response);
                return;
            }
        }
        /*
         * 排除路径,并且如果是options请求是cors跨域预请求,设置allow对应头信息
         * permitUrls可以自定义不需要验证的url
         */
        String[] permitUrls = {"/authentication"};
        for (String permitUrl : permitUrls) {
            if (antPathMatcher.match(permitUrl, request.getRequestURI())
                    || Objects.equals(RequestMethod.OPTIONS.toString(), request.getMethod())) {
                log.info("jwt不拦截此路径:{},请求方式为:{}", request.getRequestURI(), request.getMethod());
                filterChain.doFilter(request, response);
                return;
            }
        }
        // 获取请求头Authorization
        String authHeader = request.getHeader(HEADER_NAME);
        if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(TOKEN_PREFIX)) {
            log.error("Authorization的开头不是Bearer,Authorization===>{}", authHeader);
            responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "暂无权限!");
            return;
        }
        // 截取token
        String authToken = authHeader.substring(TOKEN_PREFIX.length());
        //判断token是否失效
        if (jwtTokenUtil.isTokenExpired(authToken)) {
            log.info("token已过期!");
            responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "token已过期!");
            return;
        }
        String randomKey = jwtTokenUtil.getMd5KeyFromToken(authToken);
        String username = jwtTokenUtil.getUsernameFromToken(authToken);
        //如果访问的是刷新Token的请求
        if (antPathMatcher.match("/refreshToken", request.getRequestURI()) && Objects.equals(RequestMethod.POST.toString(), request.getMethod())) {
            final String getRandomKey = jwtTokenUtil.getRandomKey();
            refreshEntity(response, HttpStatus.OK.value(), jwtTokenUtil.generateToken(username, getRandomKey), jwtTokenUtil.refreshToken(authToken, jwtTokenUtil.getRandomKey()));
            return;
        }
        /*
         * 验证token是否合法
         */
        if (StringUtils.isBlank(username) || StringUtils.isBlank(randomKey)) {
            log.info("username{}或randomKey{} 可能为null!", username, randomKey);
            responseEntity(response, HttpStatus.UNAUTHORIZED.value(), "暂无权限!");
            return;
        }
        //获得用户名信息放入上下文中
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
                    request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        // token过期时间
        long tokenExpireTime = jwtTokenUtil.getExpirationDateFromToken(authToken).getTime();

        // token还剩余多少时间过期
        long surplusExpireTime = (tokenExpireTime - System.currentTimeMillis()) / 1000;
        log.info("Token剩余时间:" + surplusExpireTime);

        filterChain.doFilter(request, response);

    }

    private void responseEntity(HttpServletResponse response, Integer status, String message) {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(status);
        ModelMap modelMap = GenerateModelMap.generateMap(status, message);
        try {
            response.getWriter().write(JSON.toJSONString(modelMap));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void refreshEntity(HttpServletResponse response, Integer status, String token, String refreshToken) {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(status);
        ModelMap modelMap = new ModelMap();
        modelMap.put("token", JwtAuthenticationTokenFilter.TOKEN_PREFIX + token);
        modelMap.put("refreshToken", JwtAuthenticationTokenFilter.TOKEN_PREFIX + refreshToken);
        try {
            response.getWriter().write(JSON.toJSONString(modelMap));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

加入过滤链

最后呢吧过滤器加入到过滤连里就可以啦

@Configuration
public class ValidateSecurityCoreConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private UserService userService;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

                    @Override
                    public String encode(CharSequence rawPassword) {
                        return MD5Util.encode((String) rawPassword);
                    }

                    @Override
                    public boolean matches(CharSequence rawPassword, String encodedPassword) {
                        return encodedPassword.equals(MD5Util.encode((String) rawPassword));
                    }
                });
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginProcessingUrl("/authentication")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .and()
                .csrf().disable();
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests().antMatchers("/authentication").permitAll();
    }
}

至此呢我们就实现了一个jwt的认证,我只是粗略的做了以下很多都没考虑进去,大家根据大体思路可以自行扩展(比如token拉黑啥的)。

注意

此外在我的github代码中还有一套关于redis处理jwt的(感觉有点本末倒置,还不如用sessionId那套配合Spring Session)

本博文是基于springboot2.x 和security 5 如果有什么不对的请在下方留言。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值