快速上手spring security的三篇文章三:JWT访问授权代码实现

本篇博文主要是对请求授权操作代码实现,代码是基于上篇博文Springboot整合Spring Security 做JWT登录认证代码实现来实现的,大家可以先看上一篇登录认证再来看这篇可能会好理解一点。

访问授权流程

spring security访问授权主要流程图:
在这里插入图片描述

根据流程图所示,我们要实现的功能代码包含如下几部分:

  • 有效授权认证信息类(Authentication)
  • 请求拦截过滤器(Filter)
  • 授权校验实现类(Provider)
  • 授权成功处理器(SuccessHandler)
  • 授权失败处理器(FailHandler)
  • 授权校验配置类(Configure)
有效授权认证类代码实现

此功能代码主要是用于传输用户请求授权携带的认证信息。

public class JwtAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 3981518947978158945L;

    private UserDetails principal;
    
    private String credentials;
    
    private DecodedJWT token;

    public JwtAuthenticationToken(DecodedJWT token) {
        super(Collections.emptyList());
        this.token = token;
    }

    public JwtAuthenticationToken(UserDetails principal, DecodedJWT token, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.token = token;
    }

    @Override
    public void setDetails(Object details) {
        super.setDetails(details);
        this.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return credentials;
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }

    public DecodedJWT getToken() {
        return token;
    }
}
访问请求过滤器代码实现

过滤器主要实现功能如下:
(1)获取到请求携带的认证信息
(2)将认证信息交给相应的认证类进行认证
(3)根据认证结果指定相应的处理类处理

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private RequestMatcher authenticationRequestMatcher;
    private List<RequestMatcher> permissiveRequestMatchers;
    private AuthenticationManager authenticationManager;

    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    //指定请求规则
    public JwtAuthenticationFilter() {
        this.authenticationRequestMatcher = new RequestHeaderRequestMatcher("Authorization");
    }

    @Override
    public void afterPropertiesSet() throws ServletException {
        Assert.notNull(authenticationManager, "authenticationManager must be specified");
        Assert.notNull(successHandler, "AuthenticationSuccessHandler must be specified");
        Assert.notNull(failureHandler, "AuthenticationFailureHandler must be specified");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //判断请求头是否携带token
        if (!authenticationRequestMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        Authentication authResult = null;
        AuthenticationException failed = null;
        try {
            String jwtToken = StringUtils.removeStart(request.getHeader("Authorization"), "Bearer ");
            if (StringUtils.isNotBlank(jwtToken)) {
                //将请求携带的认证信息封装成我们用于授权的认证对象
                JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(JWT.decode(jwtToken));
                //认证信息校验开始
                authResult = this.authenticationManager.authenticate(authenticationToken);
            } else {
                failed = new InsufficientAuthenticationException("token 不能为空");
            }
        } catch (JWTDecodeException e) {
            logger.error("jwt format error", e);
            failed = new InsufficientAuthenticationException("jwt format error", failed);
        } catch (InternalAuthenticationServiceException e) {
            logger.error(
                    "An internal error occurred while trying to authenticate the user.",
                    failed);
            failed = e;
        } catch (AuthenticationException e) {
            // Authentication failed
            failed = e;
        }
        if (authResult != null) {

            successfulAuthentication(request, response, filterChain, authResult);
        } else if (!permissiveRequest(request)) {
            unsuccessfulAuthentication(request, response, failed);
            return;
        }

        filterChain.doFilter(request, response);

    }

    /**
     * 校验成功处理
     *
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authResult);
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    /**
     * 校验失败处理
     * 
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    protected void unsuccessfulAuthentication(HttpServletRequest request,
                                              HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        failureHandler.onAuthenticationFailure(request, response, failed);
    }

    /**
     * 授权失败,判断请求是否需要处理
     * 
     * @param request
     * @return
     */
    protected boolean permissiveRequest(HttpServletRequest request) {
        if (permissiveRequestMatchers == null) {
            return false;
        }
        for (RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
            if (permissiveMatcher.matches(request)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 设置指定请求,如果请求授权失败也不影响该请求进行
     *
     * @param urls
     * @return
     */
    public void setPermissiveRequestMatchers(String... urls) {
        if (permissiveRequestMatchers == null) {
            permissiveRequestMatchers = new ArrayList<>();
        }
        for (String url : urls) {
            permissiveRequestMatchers.add(new AntPathRequestMatcher(url));
        }
    }

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
        this.successHandler = successHandler;
    }

    public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
        this.failureHandler = failureHandler;
    }
}
授权校验实现类代码实现
用户信息处理类(UserDetailsService实现类)

代码功能实现了基于auth0用户token的封装,用户登出逻辑处理。


@Component
public class SelfUserService implements UserDetailsService {

    @Autowired
    RedisTemplate<Object, Object> redisTemplate;

    //token有效期,设为半个钟
    public static final int EXPIRE_TIME = 1800000;

    @Autowired
    UserMapper userMapper;

    /**
     * 根据用户名获取用户信息
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.selectByName(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        return org.springframework.security.core.userdetails.User.builder().username(username).password(user.getPassword()).roles("USER").build();
    }

    /**
     * 将用户信息封装成用户认证jwt
     *
     * @param username
     * @return
     */
    public String getUserToken(String username) {
        Date expireTime = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        String salt = getSalt(username);
        String token = JWT.create().withSubject(username)
                .withIssuedAt(new Date())
                .withExpiresAt(expireTime)
                .sign(Algorithm.HMAC256(salt));
        return token;
    }

    /**
     * 获取加密算法密钥
     * 
     * @param username
     * @return
     */
    public String getSalt(String username){
        //先从Redis中获取
        String key = "salt" + username;
        Object saltObj = redisTemplate.opsForValue().get(key);
        String salt = "";
        //如果Redis获取密钥失败,从数据库中获取
        if(saltObj == null){
            User user = userMapper.selectByName(username);
            salt = user.getSalt();
        }else {
            salt = String.valueOf(saltObj);
        }
        //如果库里也没,重新生成密钥并将密钥更新至数据库和Redis中
        if(StringUtils.isBlank(salt)){
            salt = StringRandomUtil.generateString(15);
            userMapper.refreshSalt(username,salt);
            redisTemplate.opsForValue().set(key,salt);
        }
        return salt;
    }

    public void createUser(String username, String password) {
        String encryptPwd = SelfPasswordEncod.encode(password);
        /**
         * @todo 保存用户名和加密后密码到数据库
         */
    }

    /**
     * 账户退出,更新用户加密密钥
     * 
     * @param username
     */
    public void deleteUserLoginInfo(String username) {
        String key = "salt" + username;
        String salt = StringRandomUtil.generateString(15);
        userMapper.refreshSalt(username,salt);
        redisTemplate.opsForValue().set(key,salt);
    }
}

授权校验实现类(AuthenticationProvider实现类)
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private SelfUserService selfUserService;

    /**
     * 认证主方法
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        DecodedJWT jwt = ((JwtAuthenticationToken)authentication).getToken();
        if(jwt.getExpiresAt().before(Calendar.getInstance().getTime())){
            throw new NonceExpiredException("Token expires");
        }
        String username = jwt.getSubject();
        UserDetails user = selfUserService.loadUserByUsername(username);
        if(user == null || user.getPassword()==null){
            throw new NonceExpiredException("Token expires");
        }
        //根据用户获取用户token加密密钥
        String encryptSalt = selfUserService.getSalt(username);
        try {
            //校验用户信息,这里只是简单代码展示只校验参数用户名
            Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withSubject(username)
                    .build();
            verifier.verify(jwt.getToken());
        } catch (Exception e) {
            throw new BadCredentialsException("JWT token verify fail", e);
        }
        JwtAuthenticationToken token = new JwtAuthenticationToken(user, jwt, user.getAuthorities());
        return token;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.isAssignableFrom(JwtAuthenticationToken.class);
    }
}
授权成功处理类(AuthenticationSuccessHandler实现类)

授权成功,指定token刷新时间,判断token是否需要刷新,如果需要生成新的token传回前端。因为我们token设置的有效时长为半个钟,如果在这半个钟中用户有对系统请求访问,在一定时间内,我们需要更新其token。

@Component
public class JwtRefreshTokenHandler implements AuthenticationSuccessHandler {

    //刷新间隔5分钟
    private static final int tokenRefreshInterval = 300;

    @Autowired
    private SelfUserService jwtUserService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        DecodedJWT jwt = ((JwtAuthenticationToken)authentication).getToken();
        boolean shouldRefresh = shouldTokenRefresh(jwt.getIssuedAt());
        if(shouldRefresh) {
            String newToken = jwtUserService.getUserToken(((UserDetails)authentication.getPrincipal()).getUsername());
            response.setHeader("Authorization", newToken);
        }
    }
    protected boolean shouldTokenRefresh(Date issueAt){
        LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
        return LocalDateTime.now().minusSeconds(tokenRefreshInterval).isAfter(issueTime);
    }
}

授权校验配置类代码实现

JwtAuthenticationConfigure授权校验配置类:


@Component
public class JwtAuthenticationConfigure<T extends JwtAuthenticationConfigure<T,B>,B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T,B> {

    private JwtAuthenticationFilter authFilter;

    public JwtAuthenticationConfigure() {
        this.authFilter = new JwtAuthenticationFilter();
    }

    @Override
    public void configure(B http) throws Exception {
        authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        authFilter.setFailureHandler(new LoginFailureHandler());

        JwtAuthenticationFilter filter =postProcess(authFilter);
        http.addFilterBefore(filter, LogoutFilter.class);
    }


    public JwtAuthenticationConfigure<T, B> permissiveRequestUrls(String ... urls){
        authFilter.setPermissiveRequestMatchers(urls);
        return this;
    }

    public JwtAuthenticationConfigure<T, B> tokenValidSuccessHandler(AuthenticationSuccessHandler successHandler){
        authFilter.setSuccessHandler(successHandler);
        return this;
    }

}

整合授权校验配置类至web security配置中:


@Configuration
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {

    @Autowired
    private JsonLoginSuccessHandler jsonLoginSuccessHandler;

    @Autowired
    private JwtRefreshTokenHandler refreshTokenHandler;

    @Autowired
    private JwtAuthenticationProvider jwtAuthenticationProvider;

    @Autowired
    private UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider;

    @Autowired
    private TokenClearLogoutHandler tokenClearLogoutHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/image/**").permitAll()
                .antMatchers("/record/**").hasRole("USER")
                .and()
            .csrf().disable()
            .formLogin().disable()//禁用表单登录
            .cors()
            .and()
            .apply(new LoginConfigure<>()).loginSuccessHandler(jsonLoginSuccessHandler)
            .and()
            .apply(new JwtAuthenticationConfigure<>()).tokenValidSuccessHandler(refreshTokenHandler).permissiveRequestUrls("/logout")
            .and()
            .logout()
            //设置登出处理器,登出操作更新用户密钥
                .addLogoutHandler(tokenClearLogoutHandler)
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
            .and()
            .sessionManagement().disable();//禁用session
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(usernamePasswordAuthenticationProvider)
        .authenticationProvider(jwtAuthenticationProvider);
    }

}
postman 测试
登录

在这里插入图片描述

请求预先写好的一个接口

将登录获取的token放到请求头中,属性名为Authorization,发送请求可见请求成功。

在这里插入图片描述

登出操作后再次请求预先写好的接口,请求失败,token校验失败。登出操作我们更新了用户加密密钥,导致再次请求时,token校验失败。
在这里插入图片描述

项目地址:https://gitee.com/huangjinfa/booking.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值