关于权限,一直都存在争议,从最早的切面控制到现在的权限框架,不得不说是一种解放程序员双手的进步。
本文主要记录SpringSecurity+Jwt的配置与使用,通过不同的路径来控制不同的权限

介绍

SpringSecurity是一个能够为基于Spring的应用系统提供声明式的安全访问控制的安全框架,底层实现了一条一条过滤链路,用户请求进来,先判断用户的请求权限,再继续往下走,如果无权限的则抛出异常。

为系统提供了声明式的安全访问控制功能,极大的减少了为系统安全控制编写的重复代码。

SpringSecurity对Web安全的支持大量的依赖于Servlet过滤器。这些过滤器拦截进入请求,并在应用程序处理之前进行某些安全处理。

1.环境依赖

基于spring-boot-starter-parent 2.6.8,依赖引入

<!--Spring boot 安全框架-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-api</artifactId>
	<version>0.11.5</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-impl</artifactId>
	<version>0.11.5</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-jackson</artifactId>
	<version>0.11.5</version>
</dependency>

2.验证规则代码

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


    @Autowired
    ApplicationContext applicationContext;

//    @Bean
//    GrantedAuthorityDefaults grantedAuthorityDefaults() {
//        // 去除 hasRole ROLE_ 前缀 实测不生效 暂时不用
//        return new GrantedAuthorityDefaults("");
//    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 密码加密方式
        return new BCryptPasswordEncoder();
    }

    /**
     * 一些通用的api允许访问
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        this.getDefaultConfigure(http);
    }

    public void getDefaultConfigure(HttpSecurity http) throws Exception {
        // 获取匿名标记
        Map<String, Set<String>> anonymousUrls = this.getAnonymousUrl();
        http.csrf().disable()
                // 授权异常
                .exceptionHandling()
                //403处理类 无法访问
                .authenticationEntryPoint(getAuthenticationEntryPoint())
                //401处理类 身份信息验证错误
                .accessDeniedHandler(getAccessDeniedHandler())
                .and()
                .authorizeRequests()
                // 允许所有的白名单路径 swagger actuator等等 String[]
                .antMatchers(SecurityConstant.PASS_PATH_GROUP).permitAll()
                // 允许所有的 OPTIONS
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 自定义注解匿名访问所有url放行:允许匿名和带Token访问,细腻化到每个 Request 类型
                // GET
                .antMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
                // POST
                .antMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
                // PUT
                .antMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
                // PATCH
                .antMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
                // DELETE
                .antMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
                // 所有类型的接口都放行
                .antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
                //默认全部放行
//                .antMatchers("/**").permitAll()
                //前后台jwt中默认的权限 /服务名/api -> app接口 /服务名/admin -> 管理后台接口
                //hasRole 需要在权限面前加上ROLE_ hasAuthority/则不需要
                .antMatchers("/api/**").hasAuthority(SecurityConstant.AUTH_USER)
                .antMatchers("/admin/**").hasAuthority(SecurityConstant.AUTH_MANAGER)
                // 所有请求都需要认证
                .anyRequest().authenticated()
                //添加过滤器
                .and().addFilter(this.getDefaultFilter());
    }

    public AccessDeniedHandler getAccessDeniedHandler() {
        return (request, response, e) -> {
            log.error("403 Forbidden,URI:{}", request.getRequestURI());
            //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
            response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
        };
    }

    public AuthenticationEntryPoint getAuthenticationEntryPoint() {
        return (request, response, e) -> {
            log.error("401 无凭据,{},URI:{}", request.getMethod(), request.getRequestURI());
            // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e == null ? "Unauthorized" : e.getMessage());
        };
    }

    //默认的jwt校验器 可由子类覆盖实现
    public BasicAuthenticationFilter getDefaultFilter() throws Exception {
        return new JwtAuthorizationFilter(authenticationManager());
    }

	public Map<String, Set<String>> getAnonymousUrl() {
        // 搜寻匿名标记 url: @AnonymousAccess
        RequestMappingHandlerMapping requestMappingHandlerMapping =
                (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
        // 获取匿名标记
        Map<String, Set<String>> anonymousUrls = new HashMap<>(6);
        Set<String> get = new HashSet<>();
        Set<String> post = new HashSet<>();
        Set<String> put = new HashSet<>();
        Set<String> patch = new HashSet<>();
        Set<String> delete = new HashSet<>();
        Set<String> all = new HashSet<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
            HandlerMethod handlerMethod = infoEntry.getValue();
            AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
            if (null != anonymousAccess) {
                List<RequestMethod> requestMethods = new ArrayList<>(infoEntry.getKey().getMethodsCondition().getMethods());
                RequestMethodEnum request = RequestMethodEnum.find(requestMethods.size() == 0 ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
                switch (Objects.requireNonNull(request)) {
                    case GET:
                        get.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    case POST:
                        post.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    case PUT:
                        put.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    case PATCH:
                        patch.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    case DELETE:
                        delete.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                    default:
                        all.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
                        break;
                }
            }
        }
        anonymousUrls.put(RequestMethodEnum.GET.getType(), get);
        anonymousUrls.put(RequestMethodEnum.POST.getType(), post);
        anonymousUrls.put(RequestMethodEnum.PUT.getType(), put);
        anonymousUrls.put(RequestMethodEnum.PATCH.getType(), patch);
        anonymousUrls.put(RequestMethodEnum.DELETE.getType(), delete);
        anonymousUrls.put(RequestMethodEnum.ALL.getType(), all);
        return anonymousUrls;
    }
}

3.权限过滤器

BasicAuthenticationFilter 的继承类

@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                FilterChain chain) throws IOException, ServletException {
    String token = JwtUtils.getToken(req);
    if (token == null) {
        //可能会存在前端session缓存问题
        SecurityContextHolder.getContext().setAuthentication(null);
        chain.doFilter(req, res);
        return;
    }
    try {
        //验证token 并取出对应信息 在这里检验权限
        Authentication authentication = JwtUtils.getAuthentication(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        //子类自定义操作 例如 存储,redis过期/续期
        this.handler(token, authentication);

        chain.doFilter(req, res);
    } catch (Exception e) {
        GlobalExceptionEnum ex = GlobalExceptionEnum.TOKEN_IS_ERROR;
        logger.error(e.getMessage());
        res.setContentType("application/json; charset=utf-8");
        res.setStatus(HttpStatus.UNAUTHORIZED.value());
        res.getWriter().write("{\"code\": " + HttpStatus.UNAUTHORIZED.value() + ", \"message\": \"" + ex.getText() + "\"}");
        res.getWriter().close();
    }
}

//getAuthentication
/**
 * 依据Token 获取鉴权信息
 * @param token /
 * @return /
 */
public static Authentication getAuthentication(String token) {
    Claims claims = parseToken(token);
    List grantedAuthoritys = (List) claims.get(SecurityConstant.CLAIMS);

    List<GrantedAuthority> authorities = new ArrayList<>();
    boolean isManage = false;
    for (Object item : grantedAuthoritys) {
        Map<String, Object> map = (Map<String, Object>) item;
        authorities.add(new SimpleGrantedAuthority(map.get("authority").toString()));
        if (SecurityConstant.AUTH_MANAGER.equals(map.get("authority").toString())) {
            isManage = true;
        }
    }
    if (isManage) {
        User principal = new User(claims.getSubject(), "******", authorities);
        return new UsernamePasswordAuthenticationToken(principal, null, authorities);
    }
    //authorities 生成jwt设置
    return new UsernamePasswordAuthenticationToken(claims.getSubject(), null, authorities);
}

4.token的创建和解析

private static JwtParser jwtParser;
private static JwtBuilder jwtBuilder;
private static Key key;

static {
    /**
     * 512秘钥生成方法 jwt0.9.1以后
     * Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
     * String secretString = Encoders.BASE64.encode(key.getEncoded());
     */
    key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SecurityConstant.SECRET));
    jwtParser = Jwts.parserBuilder()
            .setSigningKey(key)
            .build();
    jwtBuilder = Jwts.builder()
            .signWith(key, SignatureAlgorithm.HS512);
}
/**
 * 不设过期时间 需要过期处理的可用到redis
 * @param username
 * @param authorities 权限列表
 * @return
 */
public static String createToken(String username, List<GrantedAuthority> authorities) {
    String token = jwtBuilder
            .setSubject(username)
            .claim(SecurityConstant.CLAIMS, authorities)
            .compact();
    return token;
}

//token解析
public static Claims parseToken(String token) {
    return jwtParser.parseClaimsJws(token).getBody();
}

5.生成jwt并授权

//授权组
List<GrantedAuthority> authorities = new ArrayList<>();
// 昵称#id#uuid
authorities.add(new SimpleGrantedAuthority(userInfo.getNickname()
        + SecurityConstant.AUTH_SEPARATOR + userInfo.getId()
        + SecurityConstant.AUTH_SEPARATOR + userInfo.getUuid()));
//权限字段
authorities.add(new SimpleGrantedAuthority(SecurityConstant.AUTH_USER));
//添加时间戳是保证每次生成token不一致 用于单点登录
authorities.add(new SimpleGrantedAuthority(new Date().getTime() + ""));
// 生成令牌
return JwtUtils.createToken(userInfo.getNickname(), authorities);

6.获取uid和检验权限

/**
 * 获取当前登录用户的UID
 *
 * @return
 */
public static Long getLoginUid() {
    List<GrantedAuthority> grantList = (List<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
    if (grantList.size() > 1) {
        String[] authoritys = grantList.get(0).getAuthority().split(SecurityConstant.AUTH_SEPARATOR);
        if (authoritys.length >= 2) {
            return Long.valueOf(authoritys[1]);
        }
    }
    return null;
}

/**
 * 是否包含该权限
 *
 * @param roleNames
 * @return
 */
public static boolean containsRole(String... roleNames) {
    List<GrantedAuthority> grantList = (List<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
    for (GrantedAuthority authority : grantList) {
        for (String roleName : roleNames) {
            if (roleName.equals(authority.getAuthority())) return true;
        }
    }
    return false;
}

7.基于框架授权模式

即登录账号密码直接传入框架,然后实现框架内的接口给予授权

//用账户密码登录的
UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(authUser.getUsername(), password);
//最终回调到UserDetailsService#loadUserByUsername 方法 该方法需要自己实现 验证用户信息和返回授权List<GrantedAuthority>
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
//能得到是user信息 授权信息 等 看自己实现
final JwtUserDto jwtUserDto = (JwtUserDto) authentication.getPrincipal();

UserDetailsService#loadUserByUsername 的实现中进行授权,最终返回一个UserDetail接口实现对象

以上就是本章的全部内容了。