呕心沥血,一整套完整的基于SpringSecurity的自定义认证、动态授权、权限控制以及注销功能的实现

一、SpringSecurity概念

1、Spring Security是基于J2EE开发的企业应用软件提供了全面的安全服务,是针对Spring项目的安全框架,也是SpringBoot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.5.1</version>
</dependency>

在这里插入图片描述

二、编写基础配置类

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

}
  1. @EnableWebSecurity注解 ,以启用Spring Security的Web安全支持,并提供Spring MVC集成。它还继承了WebSecurityConfigurerAdapter,并覆盖了一些方法来设置Web安全配置的一些细节。
  2. @EnableGlobalMethodSecurity(prePostEnabled = true)注解,当我们想要开启spring方法级安全时,只需要在任何 @Configuration实例上使@EnableGlobalMethodSecurity 注解就能达到此目的。同时这个注解为我们提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三种不同的机制来实现同一种功能

三、自定义用户认证(基于数据库的认证)

3.1、认证概念

​ Spring Security提供了很多过滤器,它们拦截Servlet请求,并将这些请求转交给认证处理过滤器和访问决策过滤器进行处理,并强制安全性认证用户身份和用户权限以达到保护WEB资源的目的,Spring Security安全机制包括两个主要的操作,认证和验证,验证也可以称为权限控制,这是Spring Security两个主要的方向,认证是为用户建立一个他所声明的主体的过程,这个主体一般是指用户设备或可以在系统中执行行动的其他系统,验证指用户能否在应用中执行某个操作,在到达授权判断之前身份的主体已经由身份认证过程建立了

3.2、基于内存认证

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //认证:可以基于数据库认证或者内存认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("qianqian").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
                .and()
                .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
    }

}

3.3、基于数据库认证 - -自定义认证

3.3.1、获取表单数据

  1. 值得注意的是,SpringSecurity原生的UsernamePasswordAuthenticationFilter仅支持账户和密码的表单参数校验,对于前端传来的如验证码等其他表单元素,并无法进行验证
    在这里插入图片描述
  2. 虽然UsernamePasswordAuthenticationFilter仅支持账户和密码校验,但是SpringSecurity为我们提供了解决办法,WebAuthenticationDetails: 该类提供了获取用户登录时携带的额外信息的功能,默认提供了 remoteAddress 与 sessionId 信息。我们还可以添加自己所需要的信息,比如验证码
  3. 但是转念一想,如果你只是继承了WebAuthenticationDetails,并且添加自己所需要的属性!但是好像并没有真正的把它告诉给SpringSecurity,说“喂,我把你原生的属性替换了!”,所以下一步我们需要把SpringSecurity原生的WebAuthenticationDetails替换成自己实现的WebAuthenticationDetails,这个时候就需要去实现AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails>
  4. 最后最最最最最重要的事一定要在SpringSecurity的配置类中让他生效
  5. 简单的画了一张流程图,加深理解
    在这里插入图片描述
  • WebAuthenticationDetails
public class UserWebAuthenticationDetails extends WebAuthenticationDetails {

    private static Logger log = LoggerFactory.getLogger(UserWebAuthenticationDetails.class);

    private static final long serialVersionUID = 6975601077710753878L;

    private final String code;

    /**
     * @param request that the authentication request was received from
     */

    public UserWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        code=request.getParameter("code");
        log.info("进入到了获取验证码的过滤器-->WebAuthenticationDetails,获取到的验证码-->"+code);

    }



    public String getCode() {
        return code;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString()).append("; code: ").append(this.getCode());
        return sb.toString();
    }
}
  • AuthenticationDetailsSource
@Component("authenticationDetailsSource")

public class UserAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    private static Logger log = LoggerFactory.getLogger(UserAuthenticationDetailsSource.class);

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        log.info("开始替换SpringSecurity原生的WebAuthenticationDetails");
        return new UserWebAuthenticationDetails(request);
    }
}
  • SecurityConfig
    这里只贴了部分代码,太多的话会导致不知道具体在干嘛,还有我自己在写的时候,这个替换规则一定要写在.formLogin()这个开启表单验证的后面
    /*
        获取登录表单额外参数
     */
    @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;

                .and()
                    .formLogin()
                    .loginProcessingUrl("/admin/user/login").permitAll()
                    .successHandler(getLoginSuccessHandler())
                    .failureHandler(getLoginFailHandler())
                    .authenticationDetailsSource(authenticationDetailsSource)
  • 至此,获取前端传来的表单元素就结束了,但是是真的结束了么?敲代码的心得就是,当我启动的时候,理想状态是他没有报错!但是现实却如此骨感,他怎么会不报错呀!长时间下来,他如果不报错我都觉得不正常!但是话说回来,这确实还有一个比较大的坑,这坑又大又圆,如果只是从后端考虑的话,确实难搞。
  • 我这个项目是前后端分离,前端是用vue写的,学过的同学应该知道,vue默认交互数据的方式是json,但是呢,这个SpringSecurity呢就离了个大谱,他默认支持的方式x-www-form-urlencoded方式的!所以我就在这被坑了很长一段时间,找到问题之后就好解决了,前端用qs转换一下数据格式就OK了,转换数据之后如果按正常流程的话,vue会自动识别数据的格式,他还是用自动转换为json数据格式,所以咱们发送请求就使用传统的请求发送就OK了,大功告成
    //登录
    login(data){
        return request.post(`/api/admin/user/login?`+data)
    },
	//发送登录请求
	user.login(qs.stringify(this.loginForm, {arrayFormat: 'indices', allowDots: true}))
	//发送的请求格式
	http://localhost:8080/api/admin/user/login?username=admin&password=admin&code=wjud

3.3.2、自定义认证流程

  1. 我们上边说到了UsernamePasswordAuthenticationFilter这个过滤器并不足以支持完成我们校验功能,所以我们需要自己去实现一个AuthenticationProvider
  2. spring security5+后密码策略变更了。必须使用 PasswordEncoder 方式,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,新版本中需要指定密码编码格式,我们需要去实现PasswordEncoder,密码加密规则有很多种,可以使用SpringSecurity原生的,也可以使用自己封装好的工具类,比如MD5等等。
  3. 所谓基于数据库的校验,那肯定得跟数据库打交道啊,咱在这一顿操作下来,发现没有一丁点数据库的事呀,介是嘛呀!所以到了这一步,没有学过的同学也该想到,SpringSecurity应该会提供一个接口让你去实现自己的查询逻辑,那么他就来了,我们需要实现UserDetailsService这个接口!这个接口内部默认需要去实现的方法Public UserDetails loadUserByUsername(String username),在这个方法内部我们就可以实现自己的查询逻辑了!但仔细一看这个方法的返回值,介是嘛呀!没见过呀,赶紧点进去看一看,哦,原来是一个接口呀!那就好说了,实现它!讲道理还是要弄清楚的,这个接口里边定义几个很重要的属性!百度一下,哦,原来如此!UserDetails ,该接口是提供用户信息的核心接口该接口实现仅仅存储用户的信息。后续会将该接口提供的用户信息封装到认证对象Authentication中去。
  4. 那我们实现这个接口,完成我们自己的的信息存储
  5. 再来一个流程图,我们完成这一阶段的工作
    在这里插入图片描述
  • AuthenticationProvider
@Component
public class LocalAuthenticationProvider implements AuthenticationProvider {

    private static Logger log = LoggerFactory.getLogger(LocalAuthenticationProvider.class);

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private DefaultPasswordEncoderUtil defaultPasswordEncoderUtil;

    @Autowired
    private RedisUtils redisUtils;



    @Override
    public Authentication authenticate(Authentication authentication) throws BadCredentialsException {
        log.info("进入自定义验证规则");
        String username = authentication.getName();
        String password = (String)authentication.getCredentials();
        UserWebAuthenticationDetails details = (UserWebAuthenticationDetails) authentication.getDetails();
        // 获取Request, 获取其他参数信息
        String code = details.getCode();
        //验证码比较
        String redisCode = (String)redisUtils.get("code");
        if(redisCode.toString().equalsIgnoreCase(code)){
            //去找自己实现UserDetails的实现类
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if(defaultPasswordEncoderUtil.matches(password,userDetails.getPassword())){
                //用户名密码校验成功
                return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
            }
            log.info("用户名或密码不正确");
            throw new BadCredentialsException("用户名或密码不正确!");
        }

        log.info("验证码不正确");
        throw new BadCredentialsException("验证码不正确!");

    }


    /**
     * supports函数用来指明该Provider是否适用于该类型的认证,如果不合适,则寻找另一个Provider进行验证处理
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        //和SpringSecurity
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}
  • PasswordEncoder
@Component
public class DefaultPasswordEncoderUtil implements PasswordEncoder {

    private static Logger log = LoggerFactory.getLogger(DefaultPasswordEncoderUtil.class);


    public DefaultPasswordEncoderUtil(){
        this(-1);
    }

    public DefaultPasswordEncoderUtil(int strLength){

    }

    /**
     * 进行MD5密码加密
     * @param rawPassword
     * @return
     */
    @Override
    public String encode(CharSequence rawPassword) {
        return MD5Utils.encrypt(rawPassword.toString());

    }

    /**
     * 进行密码比较
     * @param rawPassword
     * @param encodedPassword
     * @return
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        log.info("DefaultPasswordEncoderUtil---->前端传入的密码rawPassword==="+rawPassword.toString());
        log.info("DefaultPasswordEncoderUtil---->数据库中的密码encodedPassword==="+encodedPassword);
        return encodedPassword.equals(MD5Utils.encrypt(rawPassword.toString()));
    }
}
  • UserDetailsService
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
    private static Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private UserService userService;

    @Autowired
    private MenuService menuService;

    /**
     * 根据用户名查出用户详细信息
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        log.info("进入用户验证---->UserDetailsServiceImpl");
        //根据用户名查询
        ManUser user = userService.queryUser(username);
        log.info("查到的用户数据==="+user.toString());
        //判断用户信息
        if(user == null){
            log.info("用户不存在");
            throw new UsernameNotFoundException("用户不存在");
        }
        //查询当前用户所关联的所有菜单
        ManUserVO manUserVO = userService.queryUserRelationAllMenu(user);
        if(manUserVO==null){
            throw new UsernameNotFoundException("用户不存在");
        }
        List<ManMenu> menuList = manUserVO.getMenuList();
        ArrayList<String> menuStringList = new ArrayList<>();
        for (ManMenu manMenu : menuList) {
            menuStringList.add(manMenu.toString());
        }
        SecurityUserVO securityUserVO = new SecurityUserVO();
        securityUserVO.setCurrentUserInfo(user);
        securityUserVO.setPermissionValueList(menuStringList);

        log.info("验证数据"+securityUserVO.toString());
        //校验账户状态属性是否正常,
        return securityUserVO;
    }
}
  • UserDetails
@Data
public class SecurityUserVO implements UserDetails {

    //当前登录用户
    private transient ManUser currentUserInfo;

    //当前权限
    private List<String> permissionValueList;

    public SecurityUserVO() {
    }

    public SecurityUserVO(ManUser currentUserInfo) {
        if (currentUserInfo!=null) {
            this.currentUserInfo = currentUserInfo;
        }
    }

    /**
     * 获取角色权限
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (String permissionValue : permissionValueList) {
            if (StringUtils.isEmpty(permissionValue)) {
                continue;
            }
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
            authorities.add(authority);
        }

        return authorities;
    }


    /**
     * 获取密码
     * @return
     */
    @Override
    public String getPassword() {
        return currentUserInfo.getPassword();
    }

    /**
     * 获取用户名
     * @return
     */
    @Override
    public String getUsername() {
        return currentUserInfo.getUsername();
    }


    /**
     * 用户账号是否过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }


    /**
     * 用户账号是否被锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 用户密码是否过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 用户是否可用
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 我把我代码中的执行流程也贴一下,因为我都有在进行日志输出,所以可以直观的看到具体的执行流程
    在这里插入图片描述

3.3.3、自定义认证成功或者认证失败处理器

  1. 前面的操作让我们对认证的流程有了一定的了解,但是认证还没有结束,我们需要自定义实现认证成功和认证失败的处理器。认证成功中的业务逻辑会涉及到动态权限的管理,所以也非常重要!
  2. 自定义认证成功处理器AuthenticationSuccessHandler,认证成功之后我们需要把用户信息还有后端生成的Token反馈给前端,这里说到Token的保存方式有很多,大家可以自由选择,我这里是把用户信息和token都放到了Redis里边
  3. 自定义认证失败处理器AuthenticationFailureHandler,失败就直接返回失败原因就好了
  4. 再来一张图,一步一步的梳理整个流程,辣么多的过滤器和处理器,着实头疼!
  5. 一定要记得在SecurityConfig中把相应的服务配置进去
    在这里插入图片描述
  • AuthenticationSuccessHandler
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    private static Logger log = LoggerFactory.getLogger(LoginSuccessHandler.class);


    private JwtUtil jwtUtil;

    @Autowired
    private RedisUtils redisUtils;

    private static String signatureAlgorithmSecret="abcdefghijklmnopqrstuvwxyz123456789";

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //认证成功,得到认证成功之后的用户信息
        log.info("自定义登录处理器认证成功");
        log.info("当前的登录时间为:" + new Date().toString());
        log.info("当前的登录IP为:" + request.getRemoteAddr());

        SecurityUserVO user = (SecurityUserVO) authentication.getPrincipal();
        //根据用户名生成token
        PayloadDTO payload = jwtUtil.getPayload(user.getCurrentUserInfo());
        // 生成token
        String payloadJsonString = JSON.toJSONString(payload);
        String token;
        try {
            token = JwtUtil.generateToken(payloadJsonString, signatureAlgorithmSecret);
            //把用户名称和用户权限列表存放到redis中
            redisUtils.set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
            //返回token
            Map<String,Object> userInfoAndToken=new HashMap<>();
            userInfoAndToken.put("userInfo",user.getCurrentUserInfo());
            userInfoAndToken.put("token",token);
            ResponseUtil.out(response, Result.result("000","login success",userInfoAndToken));
        } catch (JOSEException e) {
            e.printStackTrace();
        }
    }
}
  • AuthenticationFailureHandler
public class LoginErrorHandler implements AuthenticationFailureHandler {

    private static Logger log = LoggerFactory.getLogger(LoginErrorHandler.class);

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("自定义登录处理器认证失败");
        ResponseUtil.out(response, Result.result("445","captcha error",null));
    }
}
  • SpringConfig

    /**
     * 自定义校验
     */

    @Bean
    public AuthenticationProvider authenticationProvider() {
        LocalAuthenticationProvider authenticationProvider = new LocalAuthenticationProvider();
        return authenticationProvider;
    }

    // 登录成功处理器
    @Bean
    public LoginSuccessHandler getLoginSuccessHandler() {
        return new LoginSuccessHandler();
    }


    // 登录失败处理器
    @Bean
    public LoginErrorHandler getLoginFailHandler() {
        return new LoginErrorHandler();
    }

3.3.4、总结

  1. 至此,我们的认证流程就走完了,这里给大家详细的讲了每一步及每一步出现的过滤器的作用,相信大家有所收获,抓紧去试试吧!千里之行,始于足下。万行代码,始于New对象!加油哦!

四、动态授权及权限控制

  1. SpringSecurity提供了默认的权限控制功能,需要预先分配给用户特定的权限,并指定各项操作执行所要求的权限。用户请求执行某项操作时,SpringSecurity会先检查用户所拥有的权限是否符合执行该项操作所要求的权限,如果符合,才允许执行该项操作,否则拒绝执行该项操作。
  2. 再认证的流程中我们知道,在认证成功之后,后端会跟根据用户信息生成一个token,给前端返回,那么前端接收到这个token之后,就需要为每次请求都加上这个token,token可以放到请求头中,常见的请求头方式为‘’Authorization : Basic token字符串‘’
  3. 我们在拿到token之后,想一想为什么要使用token呢?为什么不使用session呢?这里不知道的小伙伴可以去查一下。下面这张图是传统的基于的token的权限控制,可以看到这里是没有使用SpringSecurity的。
    在这里插入图片描述
  4. 我们现在讲基于SpringSecurity的。前边讲到认证通过之后,我们把用户信息和用户权限列表都存入了Redis中,那么现在我们就实现动态授权,即自定义授权。同学们现在应该有自己的想法了。前边认证我们需要去实现自定义认证器,作为授权我们也要去实现自定义授权器。我们要实现BasicAuthenticationFilter这个过滤器。仔细一看发现这个怎么这么熟悉呀,没错 BasicAuthentication不就是token的请求头么! 这个过滤的作用就是处理HTTP请求中的BASIC authorization头部,把认证结果写入SecurityContextHolder。当一个HTTP请求中包含一个名字为Authorization的头部,并且其值格式是Basic xxx时, 该Filter会认为这是一个BASIC authorization头部,这里边最重要的一个对象要来了,它贯穿整个SpringSecurity中,它就是SecurityContextHolder(Security安全上下文对象),作为程序猿,同学们听该听过特别多的什么什么上下文对象了,像什么Spring上下文对象,Application配置类上下文对象。那么这些上下文对象主要的作用是什么呢?
  5. 我们先了解一下什么为上下文对象,上下文即ServletContext,是一个全局的储存信息的空间,服务器启动,其就存在,服务器关闭,其才释放。所有用户共用一个ServletContext。所以,为了节省空间,提高效率,ServletContext中,要放必须的、重要的、所有用户需要共享的线程又是安全的一些信息。,这么一看下来,这下子同学们心里有答案了吧,我们就是要在这个BasicAuthenticationFilter过滤器的实现类中把权限列表存入上下文对象!
  6. 这一步至关重要,上边我们已经拿到了属于当前用户的权限列表,但是仅仅拿到就有用了么,细想,不就是执行了一次方法么?就这样子就可以进行授权了么?同学们发挥想象力,我们在设计数据表时是基于RBAC原则设计的,授权方式都是通过给角色授权在间接的给用户授权,我们在拿到权限列表时,是不是应该判断一下当前的请求URL即菜单的URL是不是属于当前角色呢?在这里我们提到了两个问题,一是怎么拿到当前请求URL,二是怎么判断当前请求URL属不属于当前用户所关联的角色呢?
  7. 这里我们解决第一个问题,拿到当前请求的URL!这里SpringSecurity也为我们提供了接口FilterInvocationSecurityMetadataSource 这个接口可以称为权限资源过滤器,实现动态的权限验证,它的主要责任就是当访问一个url时,返回这个url所需要的访问权限。这里我们就解决了第一个问题
  8. 开始解决第二个问题,也是最关键的!我们在上一步拿到需要的访问权限之后,理所应当现在需要进行对比,最终给他将没有权限的URL踢回去!现在就需要将存入的上下文中的权限列表跟我们这个URL所需要的权限列表进行对比,这里我们就需要使用到权限决策器AccessDecisionManager
  9. 我们统一的将没有权限的返回
  10. 理所当然,肯定要再来一张图啦! 这里呢,我们后端的权限控制就结束了,下一步就开始前端根据后端返回的权限列表实现动态路由,实时渲染!
    在这里插入图片描述
  • BasicAuthenticationFilter
public class TokenAuthFilter extends BasicAuthenticationFilter {


    private static Logger log = LoggerFactory.getLogger(TokenAuthFilter.class);


    private JwtUtil jwtUtil;

    private RedisUtils redisUtils;

    public TokenAuthFilter(AuthenticationManager authenticationManager,JwtUtil jwtUtil, RedisUtils redisUtils) {
        super(authenticationManager);
        this.jwtUtil = jwtUtil;
        this.redisUtils = redisUtils;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("已认证成功,开始授权");
        //获取当前认证成功用户权限信息
        UsernamePasswordAuthenticationToken authRequest=getAuthentication(request);
        if(authRequest != null){
            log.info("authRequest==========="+authRequest.toString());
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
        chain.doFilter(request,response);

    }

    private  UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
        String token = request.getHeader("Authorization");
        if(token != null){
            log.info("token========"+token);
            //获取用户名
            try {
                String username = JwtUtil.getUsername();
                log.info(username);
                //获取该用户所拥有的菜单信息
                List<String> permissionValueList = (List<String>) redisUtils.get(username);
                if(permissionValueList.isEmpty()){
                    return null;
                }
                Collection<GrantedAuthority> authorities=new ArrayList<>();
                for (String permissionValue : permissionValueList) {
                    SimpleGrantedAuthority auth=new SimpleGrantedAuthority(permissionValue);
                    authorities.add(auth);
                }
                return new UsernamePasswordAuthenticationToken(username,token,authorities);
            } catch (ParseException e) {
                e.printStackTrace();
            } catch (JwtSignatureVerifyException e) {
                e.printStackTrace();
            } catch (JOSEException e) {
                e.printStackTrace();

            }


        }
        return null;
    }
}
  • FilterInvocationSecurityMetadataSource
@Component
public class UserFileterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static Logger log = LoggerFactory.getLogger(UserFileterInvocationSecurityMetadataSource.class);

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Autowired
    private MenuService menuService;


    /**
     * 返回本次访问需要的权限列表
     * @param object
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        log.info("进入了安全数据源");
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        //去数据库查询资源
        List<ManMenu> allMenu = menuService.queryAllMenu();
        for (ManMenu menu : allMenu) {

            if (antPathMatcher.match(menu.getOpenAddress(), requestUrl)) {
                List<ManRole> manRoles = menuService.queryAllMenuRelationRole(menu.getMenuId());
//                List<Role> roles = menu.getRoles();
                int size = manRoles.size();
                String[] values = new String[size];
                for (int i = 0; i < size; i++) {
                    values[i] = manRoles.get(i).getRoleName();
                }
                log.info("当前访问路径是{},这个url所需要的访问权限是{}", requestUrl,values);
                return SecurityConfig.createList(values);
            }
        }
        /**
         * @Author: Galen
         * @Description: 如果本方法返回null的话,意味着当前这个请求不需要任何角色就能访问
         * 此处做逻辑控制,如果没有匹配上的,返回一个默认具体权限,防止漏缺资源配置
         **/
        log.info("当前访问路径是{},这个url所需要的访问权限是{}", requestUrl, "ROLE_LOGIN");
        return SecurityConfig.createList("ROLE_LOGIN");

    }

    /**
     * 此处方法如果做了实现,返回了定义的权限资源列表,
     * Spring Security会在启动时校验每个ConfigAttribute是否配置正确,
     * 如果不需要校验,这里实现方法,方法体直接返回null即可。
     * @return
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * 方法返回类对象是否支持校验,
     * web项目一般使用FilterInvocation来判断,或者直接返回true
     * @param clazz
     * @return
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
  • AccessDecisionManager
@Component
public class UserAccessDecisionManager implements AccessDecisionManager {

    private static Logger log = LoggerFactory.getLogger(UserAccessDecisionManager.class);

    @Autowired
    private UserService userService;
    /**
     * 先查询此用户当前拥有的权限,然后与上面过滤器核查出来的权限列表作对比,
     * 以此判断此用户是否具有这个访问权限,决定去留!所以顾名思义为权限决策器。
     * @param authentication
     * @param object
     * @param configAttributes
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {


        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            if (authentication == null) {
                throw new AccessDeniedException("当前访问没有权限");
            }
            ConfigAttribute ca = iterator.next();
            //当前请求需要的权限
            String needRole = ca.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    log.info("未登录");
                    throw new BadCredentialsException("未登录");
                }
                log.info("默认角色,没有权限");
                throw new AccessDeniedException("权限不足!");
            }
            //当前用户所具有的权限
            ManUser user = new ManUser();
            String username = authentication.getPrincipal().toString();
            user.setUsername(username);
            ManUserVO manUserVO = userService.queryUserRelationAllRoleByUsername(user);
            List<ManRole> roleList = manUserVO.getRoleList();
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (ManRole roleName : roleList) {
                if (roleName.getRoleName().equals(needRole)) {
                    return;
                }
            }
        }
        log.info("权限不足!");
        throw new AccessDeniedException("权限不足!");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}
  • AccessDeniedHandler
@Component
public class UnauthEntryPoint implements AccessDeniedHandler {


    private static Logger log = LoggerFactory.getLogger(UnauthEntryPoint.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.info("自定义未授权统一返回类 ======》 没有权限访问");
        ResponseUtil.error(response, Result.result("403","no login",null));
    }




}
  • SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private static Logger log = LoggerFactory.getLogger(SecurityConfig.class);


    private JwtUtil jwtUtil;

    private RedisUtils redisUtils;

    private DefaultPasswordEncoderUtil defaultPasswordEncoderUtil;

    private UserDetailsService userDetailsService;

    /*
        权限决策器
     */
    @Autowired
    private UserAccessDecisionManager userAccessDecisionManager;

    /*
        权限资源器
     */
    @Autowired
    private  UserFileterInvocationSecurityMetadataSource userFileterInvocationSecurityMetadataSource;

    /*
        获取登录表单额外参数
     */
    @Autowired
    private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;

    /*
        未授权统一返回类
     */
    @Autowired
    private UnauthEntryPoint unauthEntryPoint;

    @Autowired
    public SecurityConfig(UserDetailsService userDetailsService,
                          DefaultPasswordEncoderUtil defaultPasswordEncoderUtil,
                          JwtUtil jwtUtil,
                          RedisUtils redisUtils) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoderUtil = defaultPasswordEncoderUtil;
        this.jwtUtil = jwtUtil;
        this.redisUtils = redisUtils;
    }


    /**
     * 配置设置,主要配置你的登录和权限控制、以及配置过滤器链。
     *
     * @param http
     * @throws Exception
     */
    //设置退出的地址和token,redis操作地址
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        log.info("进入了配置设置");
        http.authorizeRequests().withObjectPostProcessor(
                new ObjectPostProcessor<FilterSecurityInterceptor>() {
                     @Override
                     public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                         o.setSecurityMetadataSource(userFileterInvocationSecurityMetadataSource);
                         o.setAccessDecisionManager(userAccessDecisionManager);
                         return o;
                     }
                });
        http.exceptionHandling().accessDeniedHandler(unauthEntryPoint)
                //取消csrf防护
                .and()
                    .csrf().disable()
                    //该配置是要求应用中所有url的访问都需要进行验证。我们也可以自定义哪些URL需要权限验证,哪些不需要
                    //所有请求都需要校验才能访问
                    .authorizeRequests().anyRequest().authenticated()

                .and()
                    .cors()
                //退出路径
                .and()
                    .logout().logoutUrl("/admin/user/logout")
                    // 调用退出时的处理器
                    .addLogoutHandler(new TokenLogoutHandler( redisUtils,jwtUtil))
                .and()
                    .formLogin()
                    .loginProcessingUrl("/admin/user/login").permitAll()
                    .successHandler(getLoginSuccessHandler())
                    .failureHandler(getLoginFailHandler())
                    .authenticationDetailsSource(authenticationDetailsSource)
                .and()
                // 认证过滤器
//                .addFilter(new TokenLoginFilter(jwtUtil, redisUtils,authenticationManager()))
                // 授权过滤器
                 .addFilter(new TokenAuthFilter(authenticationManager(),jwtUtil, redisUtils))
                .httpBasic();



    }


    /**
     * 调用userDetailsService和密码处理
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用SpringSecurity自己的校验规则,只能校验username和password
//        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoderUtil);
        //使用自定义校验规则
        auth.authenticationProvider(authenticationProvider());
    }

    /**
     * 不进行认证的路径,可以直接访问,此时需要携带token,不然授权会抛出异常
     * web.ignoring是直接绕开spring security的所有filter,直接跳过验证
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/user/getImage")
                .antMatchers("/user/code");

    }

    /**
     * 自定义校验
     */

    @Bean
    public AuthenticationProvider authenticationProvider() {
        LocalAuthenticationProvider authenticationProvider = new LocalAuthenticationProvider();
        return authenticationProvider;
    }

    // 登录成功处理器
    @Bean
    public LoginSuccessHandler getLoginSuccessHandler() {
        return new LoginSuccessHandler();
    }


    // 登录失败处理器
    @Bean
    public LoginErrorHandler getLoginFailHandler() {
        return new LoginErrorHandler();
    }


}

五、注销登录

  1. 这里就比较简单了,一个简单的处理器就OK了,但是处理逻辑要根据实际业务进行实现哦
public class TokenLogoutHandler implements LogoutHandler {

    private static Logger log = LoggerFactory.getLogger(TokenLogoutHandler.class);

    private RedisUtils redisUtils;

    private JwtUtil jwtUtil;

    public TokenLogoutHandler(RedisUtils redisUtils, JwtUtil jwtUtil) {
        this.redisUtils = redisUtils;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        log.info("注销登录,拦截到了注销登录的请求");
        //1、从header里面获取token
        String token = request.getHeader("Authorization");
        //2、token不为空,移除token,从Redis删除token
        if(token != null){
            try {
                //解析token里的用户信息
                PayloadDTO currentUser = jwtUtil.getJwtPayload();
                log.info("解析后的用户数据为:"+currentUser.toString());
                //将Redis里的用户信息及用户权限删除
                redisUtils.remove(currentUser.getName());
            } catch (ParseException e) {
                e.printStackTrace();
            } catch (JwtSignatureVerifyException e) {
                e.printStackTrace();
            } catch (JOSEException e) {
                e.printStackTrace();
            }
        }
    }
}

六、演示

6.1、认证

在这里插入图片描述

6.2、授权

在这里插入图片描述

6.3、注销登录

在这里插入图片描述

七、流程图

直接复制搜索就OK啦:https://www.processon.com/view/link/61b187daf346fb6a6f182ed9

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值