简账(开源记账软件)-集成Spring Security

774 阅读4分钟

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

前言

上文简单描述了什么是SpringSecurity,本文主要讲述如何在SpringBoot中集成Spring Security

往期链接

一、开发环境

开发环境如下所示:

  • Java:OpenJDK 11
  • Spring Security:5.3.3

二、集成Spring Security

此处以SpringSecurity + JWT为例

1.引入依赖

Spring-security无需指定版本,在父pom中已指定。因需要JWT所以也需要引入JJWT库

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JJWT是一个提供端到端的JWT创建和验证的Java库 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

2.配置Spring Security

主配置

主配置中主要声明了需要拦截的请求以及过滤器Filter的定义

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;


    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/user/register")
                .antMatchers("/doc.html")
                .antMatchers("/webjars/**")
                .antMatchers("/swagger-resources/**")
                .antMatchers("/v2/**")
                .antMatchers("/favicon.ico")
                // 微信获取openId的Url不拦截
                .antMatchers("/wx/openId/*")
                .antMatchers("/wx/login")
                // 登录二维码不拦截
                .antMatchers("/qrcode/**");
        web.expressionHandler(new DefaultWebSecurityExpressionHandler() {
            @Override
            protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
                WebSecurityExpressionRoot root = (WebSecurityExpressionRoot) super.createSecurityExpressionRoot(authentication, fi);
                root.setDefaultRolePrefix(""); // remove the prefix ROLE_
                return root;
            }
        });
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        return req -> {
            CorsConfiguration cfg = new CorsConfiguration();
            cfg.addAllowedHeader("*");
            cfg.addAllowedMethod("*");
            cfg.addAllowedOrigin("*");
            cfg.setAllowCredentials(true);
            cfg.checkOrigin("*");
            return cfg;
        };
    }
}

身份验证

身份验证过滤器中主要就是从请求中获取用户名以及密码,交给AuthenticationManager来处理

@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/user/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 从输入流中获取到登录的信息
        try {
            LoginVO loginVO = new ObjectMapper().readValue(request.getInputStream(), LoginVO.class);
            return authenticationManager.authenticate(
                    // 此处需要将密码加密验证
                    new UsernamePasswordAuthenticationToken(loginVO.getUsername(), loginVO.getPassword())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 成功调用的方法
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
        JWTUser jwtUser = (JWTUser) authResult.getPrincipal();
        log.info("JWTUser:{}",jwtUser.toString());
        List<String> permissionList = new ArrayList<>();
        jwtUser.getAuthorities().forEach(n -> permissionList.add(n.getAuthority()));
        // 通过获取Spring上下文来获取JWTConfig对象
        JWTConfig jwtConfig = SpringContextUtil.getBean(JWTConfig.class);
        String token = jwtConfig.createToken(jwtUser.getId().toString(),permissionList);
        UserService userService = SpringContextUtil.getBean(UserService.class);
        UserDO userDO = userService.getById(jwtUser.getId());
        LoginSuccessVO vo = new LoginSuccessVO();
        BeanUtils.copyProperties(userDO, vo);
        vo.setToken(token);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write(JSON.toJSONString(Result.success(vo)));
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write(JSON.toJSONString(Result.error(CodeMsg.LOGIN_ERROR), SerializerFeature.WriteMapNullValue));
    }
}

授权

授权过滤器主要就做一件事情:设置SecurityContext的上下文中的Authentication

@Slf4j
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader("token");
        // 校验token正确性
        try {
            checkToken(token);
            // 如果请求头中有token,则进行解析,并且设置认证信息
            SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));
            super.doFilter(request, response, chain);
        } catch (BaseException e) {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write(JSON.toJSONString(Result.error(e.getCodeMsg(), e.getMessage()), SerializerFeature.WriteMapNullValue));
        }
    }

    // 这里从token中获取用户信息并新建一个token
    private UsernamePasswordAuthenticationToken getAuthentication(String token) {
        JWTConfig jwtConfig = SpringContextUtil.getBean(JWTConfig.class);
        Claims claims = jwtConfig.getTokenClaim(token);
        if (claims != null){
            String userId = jwtConfig.getUserIdFromToken(claims);
            // 设置当前用户Id到线程中
            LocalUserId.set(Long.valueOf(userId));
            List<String> permissions = jwtConfig.getPermissions(claims);
            List<GrantedAuthority> list = new ArrayList<>();
            permissions.forEach(
                    n-> list.add(new SimpleGrantedAuthority(n))
            );
            return new UsernamePasswordAuthenticationToken(userId, null, list);
        }else {
            log.error("token格式不正确");
            throw new BusinessException(CodeMsg.JWT_EXCEPTION);
        }
    }

    /**
     * 校验token的正确性
     */
    private void checkToken(String token) {
        JWTConfig jwtConfig = SpringContextUtil.getBean(JWTConfig.class);
        if(StringUtils.isEmpty(token)){
            log.error("{}不能为空",jwtConfig.getHeader());
            throw new BusinessException(CodeMsg.JWT_EXCEPTION);
        }
        Claims claims = jwtConfig.getTokenClaim(token);
        if(claims == null){
            log.error(CodeMsg.JWT_EXCEPTION.getMessage());
            throw new BusinessException(CodeMsg.JWT_EXCEPTION);
        }
        if (jwtConfig.isTokenExpired(claims)) {
            log.error(CodeMsg.TOKEN_EXPIRED.getMessage());
            throw new ParameterException(CodeMsg.TOKEN_EXPIRED);
        }
    }

}

3.权限接口编写

此处以记账分析中的某个接口为例
@PreAuthorize("hasAuthority('record:analysis:spendCategoryTotal')")代表当前用户需拥有此权限字符

@LoginRequired
    @CommonLog(title = "获取某年所有消费类别的总额", businessType = BusinessType.QUERY)
    @ApiOperation(value = "获取某年所有消费类别的总额")
    @PreAuthorize("hasAuthority('record:analysis:spendCategoryTotal')")
    @GetMapping("/spendCategoryTotal/{year}/{recordType}")
    public Result<?> getSpendCategoryTotalInYear(@ApiParam(required = true, value = "年(yyyy)") @Validated
                                                 @DateTimeFormat(pattern="yyyy") @PathVariable(value = "year") Date date,
                                                 @NotNull(message = "记账类型编码不能为空") @PathVariable(value = "recordType")String recordType) {
        if (!recordType.equals(RecordConstant.EXPEND_RECORD_TYPE) && !recordType.equals(RecordConstant.INCOME_RECORD_TYPE)) {
            return Result.error(CodeMsg.RECORD_TYPE_CODE_ERROR);
        }
        UserDO userDO = LocalUser.get();
        List<SpendCategoryTotalDTO> list = recordDetailService.getSpendSpendCategoryTotalByYear(userDO.getId(), recordType,
                date);
        return Result.success(list);
    }

4.测试

可以成功获取到结果

image.png

三、总结

以上代码均可在简账后端中找到

感谢看到最后,非常荣幸能够帮助到你~♥