前言:SpringSecurity自带了自己的session验证、登录页面、登录方式等等功能,要想用token实现前后分离也可以用Security-JWT来实现。但是官方自带的这些功能我个人很不习惯,我还是习惯用redis和自定义token去实现权限判断。所以这次学习Security并集成到我的项目时进行了魔改(其实是官方给的接口重新实现而已)

在SecurityConfiguration配置bean继承WebSecurityConfigurerAdapter的类中实现父类configure方法,我这里这样配置的:

    protected void configure(HttpSecurity http) throws Exception {
        log.info("初始化安全框架配置...");
        //启用自定义登录页
        var httpc = http
                .formLogin()
                .loginPage("/admin/login");

        //路径白名单
        Set<String> userTokenInterceptors = applicationAuthorityBeen.whitePaths();
        userTokenInterceptors.addAll(Arrays.asList(whitePaths));
        httpc.and()
                .authorizeRequests()
                .antMatchers(userTokenInterceptors.toArray(String[]::new))
                .permitAll();

        //路径权限注册
        var httpd = httpc.and()
                .authorizeRequests();
        Map<String, Set<String>> stringSetMap = applicationAuthorityBeen.authConfig();
        for (String url : stringSetMap.keySet()) {
            httpd.antMatchers(url).hasAnyAuthority(stringSetMap.get(url).toArray(String[]::new));
        }

        // 其他的需要登录就能访问
        httpd.anyRequest().authenticated();

        // 取消跨站请求伪造防护
        http.csrf().disable();
        // 防止iframe 造成跨域
        http.headers().frameOptions().disable();

        //重写登录接口验证
        http.addFilterAt(loginAuthGetRequestFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(authenticationTokenFilter, LoginAuthGetRequestFilter.class);

        http.httpBasic().authenticationEntryPoint(securityAuthenticationEntryPoint);
        //禁用session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //无权限处理类
        http.exceptionHandling().accessDeniedHandler(securityAccessDeniedHander);
        //注销登录处理类
        http.logout().logoutUrl("/auth/logout").logoutSuccessHandler(securityAccessLogoutHander);
    }

这里面很多处理类接下来会一一说明
首先是applicationAuthorityBeen这个类主要通过自定义注解来获取路径白名单(whitePaths())和权限注册(authConfig())的集合,更方便对配置初始化操作,下面是相关代码

	 /**
     * 检查所有been获取不需要拦截的ControllerPath
     *
     * @return 所有白名单路径
     * @throws Exception
     */
    public Set<String> whitePaths() throws Exception {
        Map<String, Object> Beans = applicationContext.getBeansWithAnnotation(RestController.class);
        log.info("获取ControllerPath白名单...");
        Set<String> whitePaths = new HashSet<>();
        for (String key : Beans.keySet()) {
            //接口类名
            Class clz = AopTargetUtils.getTarget(Beans.get(key)).getClass();
            //获取其方法
            for (Method method : clz.getMethods()) {
                //权限注解对应
                PathMatcher pathMatcher = method.getAnnotation(PathMatcher.class);
                if (pathMatcher != null) {
                    //该接口是否是白名单
                    if (pathMatcher.white()) {
                        //获取访问地址
                        var ApiPath = getControllerPath(clz, method);
                        if (StrUtil.isNotBlank(ApiPath))
                            whitePaths.add(ApiPath);
                    }
                }
            }
        }
        return whitePaths;
    }

它的注解是PathMatcher,可以在controller的方法上添加实现该方法的白名单过滤

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PathMatcher {

    //true为不过滤路径
    boolean white() default true;

}

在Controller使用,比如登录页面需要加入白名单:
 @GetMapping("/login")
 @PathMatcher
 public ModelAndView login() {
    return JumpPage("login");
 }
就OK了

authConfig()方法是获取controller的权限,就像shiro那样在注解直接写上权限来控制

public Map<String, Set<String>> authConfig() throws Exception {
        log.info("获取Controller权限配置...");
        Map<String, Object> Beans = applicationContext.getBeansWithAnnotation(RestController.class);
        Map<String, Set<String>> authConfig = new HashMap<>();
        for (String key : Beans.keySet()) {
            //接口类名
            Class clz = AopTargetUtils.getTarget(Beans.get(key)).getClass();
            //获取其方法
            for (Method method : clz.getMethods()) {
                //权限注解对应
                AccessAuth accessRole = method.getAnnotation(AccessAuth.class);
                if (accessRole != null) {
                    //获取访问地址
                    var ApiPath = getControllerPath(clz, method);
                    if (StrUtil.isNotBlank(ApiPath)) {
                        Set<String> auths = new HashSet<>();
                        //接口的权限
                        String auth = accessRole.auth();
                        if (StrUtil.isNotBlank(auth))
                            auths.add(auth);
                        //该接口是否是ADMIN平台
                        if (accessRole.admin()) {
                            if (StrUtil.isBlank(auth))
                                auths.add("admin");
                            auths.add("superAdmin");
                        } else
                            auths.add("app");
                        authConfig.put(ApiPath, auths);
                    }
                }
            }
        }
        return authConfig;
    }

在这里我通过判断是否是admin平台的controller来进行给予基本权限,它的注解是AccessAuth,下面是它的代码

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AccessAuth {

    //权限说明
    String comment() default "";

    //权限值
    String auth() default "";

    //是否是admin权限
    boolean admin() default true;
}

在Controller使用:
   1、只要是admin账号都能访问
    @GetMapping("/index")
    @AccessAuth(comment = "后台首页")
    public ModelAndView index() {
        return JumpPage("sys/console");
    }
   2、细分admin权限
    @GetMapping("/index")
    @AccessAuth(auth = "admin:index",comment = "后台首页")
    public ModelAndView index() {
        return JumpPage("sys/console");
    }
   3、非admin权限
    @GetMapping("/index")
    @AccessAuth(auth = "app:index",comment = "首页",admin = false)
    public ModelAndView index() {
        return JumpPage("index");
    }
等等排列组合

以上通过自定义注解初始化权限就完事了,接下来是重写登录接口,原来官方的接口是formdata提交方式就很不'现代'hh 必须要改!LoginAuthGetRequestFilter这个类就是重写登录接口的类

1、在配置类中添加Bean:
    @Bean
    LoginAuthGetRequestFilter loginAuthGetRequestFilter() throws Exception {
        LoginAuthGetRequestFilter filter = new LoginAuthGetRequestFilter();

        filter.setAuthenticationSuccessHandler(securityAccessSuccessHander);
        filter.setAuthenticationFailureHandler(securityAccessFailureHander);
        filter.setFilterProcessesUrl("/auth/login");
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }
2、实现LoginAuthGetRequestFilter类
public class LoginAuthGetRequestFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
            String username = null;
            String password = null;
            try {
                Map<String, String> requestValue = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                username = requestValue.get("username");
                password = requestValue.get("password");
                var remember = requestValue.get("remember");
                request.setAttribute("username", username);
                request.setAttribute("remember", remember);
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (StrUtil.isBlank(username)) {
                throw new AuthenticationServiceException("账号不能为空");
            } else if (username.length() > 30) {
                throw new AuthenticationServiceException("账号不能超过30字符");
            }
            if (StrUtil.isBlank(password)) {
                throw new AuthenticationServiceException("密码不能为空");
            } else if (password.length() > 20) {
                throw new AuthenticationServiceException("账号不能超过20字符");
            }
            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
      
    }
}

这样就可以通过json把用户名密码传进来了,然后就是登录成功生成token了,其实这个没啥好说的根据自己的习惯去生成就行了,就在AuthenticationSuccessHandler这个接口实现基本就行了,都是基本操作就不费口舌了。接下来就是每次请求要验证的token操作了,authenticationTokenFilter这个类

public class AuthenticationTokenFilter extends OncePerRequestFilter {

    private final Redis redis;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        var token = AccessTokenUtil.getTokenKey(request);

        //获取用户信息
        if (StrUtil.isNotBlank(token)) {
         
               //获取到token了,就把这个用户信息解析出来吧!
		SysUser userDetails =?;
	
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                //设置上下文为已登录
                SecurityContextHolder.getContext().setAuthentication(authentication);
    
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

这里我把token获取的方式封装了(AccessTokenUtil.getTokenKey),其实就是按照你自己的token传递方式来获取就行,header、cookie或直接传值都行,只要能取到就看你自己了
剩下的配置就是security基本操作了,总结一下,这通操作完实现了以下几个功能:
1、注解白名单自动注册到配置
2、根据我的需要实现注解权限控制
3、可以通过application/json传参来登录
4、实现定制化token生成和验证

完美!不仅是web模板开发还是分离开发都没问题