Spring Security权限控制(三)

学生程序设计能力提升平台 Spring Security的应用(三)

JSON WEB TOKEN与spring security

json web token简介

JWT 是一个很长的字符串,由 . 分割为三段

Header(头部)

存储 JWT 的元数据,与生成 Token API jwt.sign(payload, secretOrPrivateKey, [options, callback])中的 options 对应

Payload(负载数据)

存放需要实际传递的数据,官方定义了7个官方字段

Signature(签名)

是对前两部分的签名,防止数据篡改

JwtUtil工具类

/**
 * JWT工具类
 * @author widealpha
 * @date 2021/7/13
 */
public class JwtUtil {
    private static final String SECRET = "secret"; //JWT签证密钥
    private static final String ROLE = "ROLE"; //Jwt中携带的身份key
    private static final String USER_ID = "USER_ID"; //Jwt中携带的用户ID的key
    private static final Long EXPIRATION = 60 * 60 * 24 * 7L; //过期时间7天
    public static final String TOKEN_HEADER = "Authorization"; //Header标识JWT
    public static final String TOKEN_PREFIX = "Bearer "; //JWT标准开头,注意空格

    /**
     * 创建JWT
     * @param username 账户名
     * @param userId 用户ID
     * @param roles 用户角色,以英文逗号(,)分隔开
     * @return 创建好的Token
     */
    public static String createToken(String username,Integer userId, String roles) {
        Map<String, Object> map = new HashMap<>();
        map.put(ROLE, roles);
        map.put(USER_ID, userId);
        return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
                .setClaims(map).setSubject(username).setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .compact();
    }

    /**
     * 根据token获取用户名
     * @param token JWT
     * @return 用户名
     */
    public static String getUsername(String token) {
        try {
            return getTokenBody(token).getSubject();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     *  获取Token载体信息
     * @param token JWT
     * @return token携带的claim
     */
    private static Claims getTokenBody(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 获取token携带的用户角色列表
     * @param token JWT
     * @return 用户角色,以英文逗号(,)分隔开
     */
    public static String getUserRole(String token) {
        return (String) getTokenBody(token).get(ROLE);
    }

    /**
     * 获取token携带的用户ID
     * @param token JWT
     * @return 用户ID
     */
    public static Integer getUserId(String token){
        return (Integer) getTokenBody(token).get(USER_ID);
    }

    /**
     * 判断token是否国企
     * @param token JWT
     * @return 是否过期
     */
    public static Boolean isExpiration(String token) {
        try {
            return getTokenBody(token).getExpiration().before(new Date());
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return true;
    }

    /**
     * 获取签发日期
     * @param token JWT
     * @return 签发Token的日期
     */
    public static Date getIssuedAt(String token){
        return getTokenBody(token).getIssuedAt();
    }
}

通过工具类可以十分便捷的获取到所有的token的属性等数据

Config

依然首先是通过config进行注入配置

@Configuration
public class JwtLoginConfig<T extends JwtLoginConfig<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {

    private final JwtAuthenticationFilter authFilter;

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

    @Override
    public void configure(B http) {
        authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
        //将filter放到logoutFilter之前
        JwtAuthenticationFilter filter = postProcess(authFilter);
        http.addFilterBefore(filter, LogoutFilter.class);
    }

    //设置匿名用户可访问url
    public JwtLoginConfig<T, B> permissiveRequestUrls(String... urls) {
        authFilter.setPermissiveUrl(urls);
        return this;
    }

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

在这里代码里把filter放置在Logout之前,是因为logout是需要鉴权的操作,每次logout都必须有授权才能logout

这里通过配置filter实现登录请求的拦截

对一些数据的实体类进行IOC控制反转,降低内存的使用

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final RequestHeaderRequestMatcher requiresAuthenticationRequestMatcher;
    private List<RequestMatcher> permissiveRequestMatchers;
    private AuthenticationManager authenticationManager;

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

    public JwtAuthenticationFilter() {
        //拦截header中带Authorization的请求
        this.requiresAuthenticationRequestMatcher = new RequestHeaderRequestMatcher("Authorization");
    }

    protected String getJwtToken(HttpServletRequest request) {
        return request.getHeader(JwtUtil.TOKEN_HEADER);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (!requiresAuthentication(request)) {
            chain.doFilter(request, response);
            return;
        }
        String tokenHeader = getJwtToken(request);
        if (tokenHeader == null || !tokenHeader.startsWith(JwtUtil.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        try {
            if (!JwtUtil.isExpiration(tokenHeader.replace(JwtUtil.TOKEN_PREFIX, ""))) {
                UsernamePasswordAuthenticationToken authentication = getAuthentication(tokenHeader);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (ExpiredJwtException e) {
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-Type", "text/html;charset=UTF-8");
            response.getWriter().print(ResultEntity.error(StatusCode.USER_TOKEN_OVERDUE));
            return;
        } catch (JwtException e) {
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-Type", "text/html;charset=UTF-8");
            response.getWriter().print(ResultEntity.error(StatusCode.USER_TOKEN_ERROR));
            return;
        } catch (Exception e) {
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-Type", "text/html;charset=UTF-8");
            response.getWriter().print(ResultEntity.error(StatusCode.COMMON_FAIL));
            return;
        }
        chain.doFilter(request, response);
    }

    //获取用户信息
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        String token = tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "");
        String account = JwtUtil.getUsername(token);
        Integer userId = JwtUtil.getUserId(token);
        if (account == null || userId == null) {
            return null;
        }
        // 获得权限 添加到权限上去
        String roles = JwtUtil.getUserRole(token);
        PtaUser ptaUser = new PtaUser(account, "[PROTECTED]", AuthorityUtils.commaSeparatedStringToAuthorityList(roles));
        ptaUser.setUserId(userId);
        return new UsernamePasswordAuthenticationToken(ptaUser, null, AuthorityUtils.commaSeparatedStringToAuthorityList(roles));
    }

    protected boolean requiresAuthentication(HttpServletRequest request) {
        return requiresAuthenticationRequestMatcher.matches(request);
    }

    protected AuthenticationManager getAuthenticationManager() {
        return authenticationManager;
    }

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


    protected boolean permissiveRequest(HttpServletRequest request) {
        if (permissiveRequestMatchers == null)
            return false;
        for (RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
            if (permissiveMatcher.matches(request))
                return true;
        }
        return false;
    }

    public void setPermissiveUrl(String... urls) {
        if (permissiveRequestMatchers == null)
            permissiveRequestMatchers = new ArrayList<>();
        for (String url : urls)
            permissiveRequestMatchers.add(new AntPathRequestMatcher(url));
    }

    public void setAuthenticationSuccessHandler(
            AuthenticationSuccessHandler successHandler) {
        Assert.notNull(successHandler, "successHandler cannot be null");
        this.successHandler = successHandler;
    }

    public void setAuthenticationFailureHandler(
            AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
    }

    protected AuthenticationSuccessHandler getSuccessHandler() {
        return successHandler;
    }

    protected AuthenticationFailureHandler getFailureHandler() {
        return failureHandler;
    }

首先通过重写filter对指定的数据进行拦截

通过config配置拦截器的具体请求,这里是将符合的请求(header中携带有anthentication的)拦截下来,接着通过header获取到token

这里获取token的时候,进行了一次jwt签证的校验,通过exception来判断验证的结果

当验证通过的时候,从jwt的payload中读取用户名和权限信息,将用户名和权限信息,放置到UsernameAuthenticationToken容器中,接着将生成好的容器注入到SercurityContextHolder中

这里我们读一下SercurityContextHolder的源码

final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	private static SecurityContext contextHolder;

	@Override
	public void clearContext() {
		contextHolder = null;
	}

	@Override
	public SecurityContext getContext() {
		if (contextHolder == null) {
			contextHolder = new SecurityContextImpl();
		}
		return contextHolder;
	}

	@Override
	public void setContext(SecurityContext context) {
		Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
		contextHolder = context;
	}

	@Override
	public SecurityContext createEmptyContext() {
		return new SecurityContextImpl();
	}

}

这里通过全局的唯一实例进行注入,所以这次设置之后,在系统的任何地方调用SercurityContextHolder均会获取到用户名和权限信息

这里我认为原来的代码这一部分并没有完全使用spring security,按照security的思路,更适合使用authentication->provider->manager的一整套流程。我会在另一篇播客里写一下思路

Refresh

JWT可以减轻服务器的负担,解决一部分的跨域难题,但是同时,为保证安全性jwt的签发是有一定的时间的,在时间将要过期的时候,我们应当及时的为jwt刷新token

public class JwtRefreshSuccessHandler implements AuthenticationSuccessHandler {

    private static final int tokenRefreshInterval = 24;  //刷新间隔1天

    public JwtRefreshSuccessHandler() {
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String tokenHeader = request.getHeader(JwtUtil.TOKEN_HEADER);
        boolean shouldRefresh = shouldTokenRefresh(JwtUtil.getIssuedAt(tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "")));
        if (shouldRefresh) {
            String roles = authentication.getAuthorities().stream()
                    .map(Object::toString).collect(Collectors.joining(","));
            String newToken = JwtUtil.createToken(authentication.getName(), ((PtaUser) authentication.getPrincipal()).getUserId(), roles);
            response.setHeader("Authorization", newToken);
        }
    }

    protected boolean shouldTokenRefresh(Date issueAt) {
        LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
        return LocalDateTime.now().minusHours(tokenRefreshInterval).isAfter(issueTime);
    }

}

在项目中采用的是successHandler的方式,当jwt校验成功之后,会自动调用这个handler,完全可以在handler中检验日期是否临近截至日期

在这里,当距离截至日期的时间不足一个tokenRefreshInteval的时候就会触发刷新,刷新的机制也很简单,将刷新有效期之后的token放置到header中

客户端只需要检测是否有authentication就可以判断是不是需要更新本地的token,更新token的操作也可以通过不侵入的拦截器操作进行

controller

controller层只需要通过hasRole注解即可进行

    @PostMapping("/getProblemByAuthorId")
    @PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_TEACHER')")
    public ResultEntity getProblemByAuthorId(@RequestParam("page") int page, @RequestParam("size") int size) throws ServletException, IOException {
        if (UserUtil.getCurrentUserId() == null) {
            return ResultEntity.error(StatusCode.NO_PERMISSION);
        }
        try {
            Pager<Problem> problemPager = problemService.getProblemByAuthorId(UserUtil.getCurrentUserId(), page, size);
            return ResultEntity.success("ok", problemPager);
        } catch (Exception e) {
            log.error("获取题目失败", e);
            return ResultEntity.error("error");
        }

    }

在没有登录,或者是登录时候检测权限的过程中没有发现对应的ADMIN或者TEACHER的,就会被403拦下来,剩下的请求才可以正常的通过进行请求的校验。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值