SpringSecurity 入门 (三)

2021-08-09 17:39:00 浏览数 (1)

废话不多说,直接开始这个SpringSecurity的学习项目。

数据库说明

用户信息表 USER 储存用户信息
代码语言:javascript复制
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
    `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'id',
    `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '名称',
    `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '密码',
    `isEnable` bit(1) NULL DEFAULT NULL COMMENT '是否启用1、启用 0、禁用',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
权限表PURVIEW储存权限信息
代码语言:javascript复制
DROP TABLE IF EXISTS `purview`;
CREATE TABLE `purview`  (
    `id` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '权限id',
    `authority` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '权限名称',
    `role` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '角色',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
用户权限表PURVIEW储存用户权限
代码语言:javascript复制
DROP TABLE IF EXISTS `authority`;
CREATE TABLE `authority`  (
    `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户权限id',
    `authority` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '权限id',
    `member_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '用户id',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

基础的Security安全配置

  1. 配置权限给SpringSecurity
  2. 配置是否需要拦截的请求
  3. 配置请求处理(success/error)
如何配置?

SecurityVerificationConfiguration配置类也是最为核心的一个类,在其中配置了关于上面的一些信息

代码语言:javascript复制
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityVerificationConfiguration extends WebSecurityConfigurerAdapter {

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

    /**
     * 拦截器
     */
    @Autowired
    public JwtAuthenticationFilter jwtAuthenticationFilter;

    /**
     * jwt 验证处理器
     */
    @Autowired
    public JwtAccessDeniedHandler jwtAccessDeniedHandler;

    /**
     * toekn 配置
     */
    @Autowired
    public TokenConfiguration tokenConfiguration;

    @Autowired
    PurviewService purviewService;
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 授权 、 验证
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 添加权限
        selectPurview(http);

        http
                .authorizeRequests()
                // 授权地址不需要验证
                .antMatchers("/auth/token").permitAll()
                // 用户注册地址
                .antMatchers("/user/registered").permitAll()
                // 其余的都需要校验
                .anyRequest().authenticated()
                .and()
                // 添加后置处理拦截器
                .addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                // 访问拒绝处理程序
                .accessDeniedHandler(jwtAccessDeniedHandler)
                .and()
                .apply(tokenConfiguration)
                .and()
                // 取消 session 的状态
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable();
    }

    /**
     * 查询权限并将权限放入 security 中
     *
     * @param http
     * @throws Exception
     */
    public void selectPurview(HttpSecurity http) throws Exception {
        List<Purview> purviews = purviewService.selectList();
        for (Purview purview : purviews) {
            http.authorizeRequests()
                    .antMatchers(purview.getAuthority()).hasAnyAuthority(purview.getRole());
        }
    }

}

关于SecurityVerificationConfiguration说明希望帮你解决一些疑惑

  • @EnableWebSecurity注解开启web安全验证
  • @EnableGlobalMethodSecurity(prePostEnabled=true)启用基于注释的安全性 参考官方文档11.5 开启后可以通过 @PreAuthorize注解限制请求controller的访问权限
  • selectPurview(HttpSecurity http)方法查询所有的权限角色,并将权限角色交由SpringSecurity管理
  • PasswordEncoder密码加密 参考官方文档5.1.2
  • JwtAuthenticationFilterTOEKM拦截器

看了基础的配置,按照 SpringSecurity 入门(二)中提到的思路,需要构建 Authentication认证对象,那么下面就是构建认证所需要的 Authentication认证对象

如何构建?

在构建 Authentication认证对象之前,还需要明确一个问题,构建Authentication认证对象所需要的信息:

  1. principal 显然这个使用final修饰不可以修改,所以传递的值一定是在认证之后不需要修改的,例如:用户信息
  2. credentials用于防止认证的信息,可以是TOKEN
  3. authorities权限集合

principal 很简单构建一个用户就可以了,像这样

代码语言:javascript复制
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) 

credentials就更简单了,把jwt生成的 TOKEN放进去就好了

authorities权限集合,也不难,只需要查找用户拥有的权限就可以,这里有两种方式,一种是获取通过用户信息获取用户的权限,还有一种就是通过TOKEN获取权限信息,但是第一种的限制比较而言更多,第二种也更为方便,所有我们通过解析TOKEN来获取用户的权限

那首先应该如何构建这个TOKEN是首要的

构建生成TOKEN
代码语言:javascript复制
@Component
@Slf4j
public class JwtUtil {

    /**
     * 签名密钥
     */
    @Value("${auth.token.signingKey}")
    private String signingKey;

    /**
     * 创建生成 token
     * <p>
     * setClaims() 与 setSubject() 冲突所以不设置主体信息
     *
     * @param claim 用户权限 map
     * @return String 生成的 token
     */
    public String createToken(Map<String, Object> claim) {
        return Jwts.builder()
                // 设置唯一的 ida
                .setId(IdUtil.simpleUUID())
//                .claim("auth", "admin")
                .setClaims(claim)
                // 设置过期时间
                .setExpiration(new DateUtil().getNowDateOneTime())
                // 设置 token 签发的时间
                .setIssuedAt(new DateTime())
                // 设置签名 使用HS256算法,并设置SecretKey(字符串)  签名算法和秘钥
                .signWith(SignatureAlgorithm.HS256, signingKey)
                // 以下内容构建JWT并将其序列化为紧凑的,URL安全的字符串
                .compact();
    }
    
}    

这里需要注意区分一下claim(String , Object)setClaims(Map<String, Object>)

  • claim只能设置单个权限
  • setClaims可以设置多个权限对象

这里建议第二种,这样可以从TOKEN中存放更多的有效信息

解析TOKEN构建Authentication认证对象
代码语言:javascript复制
@Slf4j
@Component
public class TokenProvider {

    // 权限密钥
    private static final String AUTHORITIES_KEY = "auth";

    // 用户信息
    private static final String ID = "id";

    // 签名密钥
    @Value("${auth.token.signingKey}")
    private String signingKey;

    @Autowired
    JwtUtil jwtUtil;

    /**
     * 获取 Spring Context 的 SecurityContext 对象
     * 用于获取用户的身份验证
     *
     * @param token jwt 生成的 token 信息
     * @return authentication 认证对象
     */
    public Authentication getAuthentication(String token) {

        // parser() 解析token
        Claims claims = Jwts.parser()
                .setSigningKey(signingKey)
                .parseClaimsJws(token)
                .getBody();
        Object claim = claims.get(AUTHORITIES_KEY);

        // 权限
        String auth = "";
        if (Objects.nonNull(claim)) {
            auth = claim.toString();
        }

        // 权限集合
        Collection<? extends GrantedAuthority> authorities =
                Arrays.stream(auth.split(","))
                        .filter(StringUtils::isNotBlank)
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        // 创建 Spring Security 的 user 对象
        User principal = new User((String) claims.get(ID), "", authorities);

        // 创建返回 Authentication 对象
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

}

需要先从TOKEN中获取Claims对象,并且获取权限信息,先构建 principal用户信息,再通过用户信息构建一个Authentication认证对象,到这里基本上就完成了80%了,依照先前的思路完成了代码,但是有几个问题需要考虑

  1. 在什么时候构建TOKEN信息 当然是在用户登录的时候构建这样的一个安全认证信息的令牌,并且在访问时需要携带该令牌
  2. 在什么时候解析TOKEN信息,构建Authentication认证对象 当然是在每一次访问接口的时候

权限、用户信息,现在都交给Spring Security管理了,但是怎么实现在每一次访问接口的时候去构建这个Authentication认证对象呢?

提醒一下,拦截器,OncePerRequestFilter可以确保一次请求只会通过一次该过滤

过滤拦截请求
代码语言:javascript复制
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    /**
     * 用户的业务逻辑层
     */
    @Autowired
    public UserService userService;

    @Autowired
    public JwtUtil jwtUtil;

    @Autowired
    private TokenProvider tokenProvider;

    public JwtAuthenticationFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    /**
     * 与{@code doFilter}的合同相同,但保证在单个请求线程中每个请求仅被调用一次。
     * 有关详细信息,请参见{@link #shouldNotFilterAsyncDispatch()}。
     * <p>提供HttpServletRequest和HttpServletResponse参数,而不是默认的ServletRequest和ServletResponse参数。
     *
     * @param httpServletRequest
     * @param httpServletResponse
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws IOException, ServletException {
         if (!"/auth/token".equals(httpServletRequest.getRequestURI())) {
            String token = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);
            if (token == null)
                throw new TokenException(HttpStatus.HTTP_FORBIDDEN, "缺少验证信息");

            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

}

需要排除一些接口,例如登录认证接口,其余都需要构建这样Authentication认证对象

这就是按照我们思路实现的一套 认证,具体的操作表的代码就不展示了,按照思路往下去走就可以了。

Shao Jie :代码可能有些漏洞、BUG等等一些问题,逻辑也可能不够完美,只是提供一些思路,仅供参考,具体需要怎么做,自行参考官方文档,官方有更详细的解释,只是单纯的希望,能够给你帮助,代码问题可以在GitHubISSUES

0 人点赞