• SpringBoot3安全管理


    标签:Security.登录.权限;

    一、简介

    SpringSecurity组件可以为服务提供安全管理的能力,比如身份验证、授权和针对常见攻击的保护,是保护基于spring应用程序的事实上的标准;

    在实际开发中,最常用的是登录验证和权限体系两大功能,在登录时完成身份的验证,加载相关信息和角色权限,在访问其他系统资源时,进行权限的验证,保护系统的安全;

    二、工程搭建

    1、工程结构

    2、依赖管理

    starter-security依赖中,实际上是依赖spring-security组件的6.1.1版本,对于该框架的使用,主要是通过自定义配置类进行控制;

    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
        <version>${spring-boot.version}version>
    dependency>
    

    三、配置管理

    1、核心配置类

    在该类中涉及到的配置非常多,主要是服务的拦截控制,身份认证的处理流程以及过滤器等,很多自定义的处理类通过该配置进行加载;

    @EnableWebSecurity
    @EnableMethodSecurity
    @Configuration
    public class SecurityConfig {
    
        /**
         * 基础配置
         */
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
            // 配置拦截规则
            httpSecurity.authorizeHttpRequests(authorizeHttpRequests->{
                authorizeHttpRequests
                        .requestMatchers(WhiteConfig.whiteList()).permitAll()
                        .anyRequest().authenticated();
            });
            // 禁用默认的登录和退出
            httpSecurity.formLogin(AbstractHttpConfigurer::disable);
            httpSecurity.logout(AbstractHttpConfigurer::disable);
            httpSecurity.csrf(AbstractHttpConfigurer::disable);
    
            // 异常时认证处理流程
            httpSecurity.exceptionHandling(exeConfig -> {
                exeConfig.authenticationEntryPoint(authenticationEntryPoint());
            });
    
            // 添加过滤器
            httpSecurity.addFilterAt(authTokenFilter(),CsrfFilter.class);
            return httpSecurity.build() ;
        }
    
        @Bean
        public BCryptPasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public AuthenticationEntryPoint authenticationEntryPoint() {
            return new AuthExeHandler();
        }
    
        @Bean
        public OncePerRequestFilter authTokenFilter () {
            return new AuthTokenFilter();
        }
    
        /**
         * 认证管理
         */
        @Bean
        public AuthenticationManager authenticationManager() {
            return new ProviderManager(authenticationProvider()) ;
        }
    
        /**
         * 自定义用户认证流
         */
        @Bean
        public AbstractUserDetailsAuthenticationProvider authenticationProvider() {
            return new AuthProvider() ;
        }
    }
    

    2、认证数据源

    UserDetailsService是加载用户特定数据的核心接口,编写用户服务类并实现该接口,提供用户信息和权限体系的数据查询和加载,作为用户身份识别的关键凭据;

    @Service
    public class UserService implements UserDetailsService {
    
        @Resource
        private UserBaseMapper userBaseMapper;
        @Resource
        private BCryptPasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
            UserBase queryUser = geyByUserName(userName);
            if (Objects.isNull(queryUser)){
                throw new AuthException("该用户不存在");
            }
            List grantedAuthorityList = new ArrayList<>() ;
            grantedAuthorityList.add(new SimpleGrantedAuthority(queryUser.getUserRole())) ;
            return new User(queryUser.getUserName(),queryUser.getPassWord(),grantedAuthorityList);
        }
    
        public int register (UserBase userBase){
            if (!Objects.isNull(userBase)){
                userBase.setPassWord(passwordEncoder.encode(userBase.getPassWord()));
                userBase.setCreateTime(new Date()) ;
                return userBaseMapper.insert(userBase) ;
            }
            return 0 ;
        }
    
        public UserBase getById (Integer id){
            return userBaseMapper.selectById(id) ;
        }
    
        public UserBase geyByUserName (String userName){
            List userBaseList = new LambdaQueryChainWrapper<>(userBaseMapper)
                    .eq(UserBase::getUserName,userName).last("limit 1").list();
            if (userBaseList.size() > 0){
                return userBaseList.get(0) ;
            }
            return null ;
        }
    }
    

    3、认证流程

    自定义用户名和密码的身份令牌认证逻辑,基于用户名Username从上面的用户服务类中加载数据并校验,在验证成功后将用户的身份令牌返回给调用者;

    @Component
    public class AuthProvider extends AbstractUserDetailsAuthenticationProvider {
        private static final Logger log = LoggerFactory.getLogger(AuthProvider.class);
        
        @Resource
        private UserService userService;
        @Resource
        private BCryptPasswordEncoder passwordEncoder;
    
        @Override
        protected void additionalAuthenticationChecks(
                UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            User user = (User) userDetails;
            String loginPassword = authentication.getCredentials().toString();
            log.info("user:{},loginPassword:{}",user.getPassword(),loginPassword);
            if (!passwordEncoder.matches(loginPassword, user.getPassword())) {
                throw new AuthException("账号或密码错误");
            }
            authentication.setDetails(user);
        }
        @Override
        protected UserDetails retrieveUser(
                String username, UsernamePasswordAuthenticationToken authentication)
                throws AuthenticationException {
            log.info("username:{}",username);
            return userService.loadUserByUsername(username);
        }
    }
    

    4、身份过滤器

    通过继承OncePerRequestFilter抽象类,实现用户身份的过滤器,如果不是白名单请求,需要验证令牌是否正确有效,SecurityContextHolder默认状态下使用ThreadLocal存储信息;

    @Component
    public class AuthTokenFilter extends OncePerRequestFilter {
        @Resource
        private AuthTokenService authTokenService ;
        @Resource
        private AuthExeHandler authExeHandler ;
    
        @Override
        protected void doFilterInternal(@Nonnull HttpServletRequest request,
                                        @Nonnull HttpServletResponse response,
                                        @Nonnull FilterChain filterChain) throws ServletException, IOException {
            String uri = request.getRequestURI();
            if (Arrays.asList(WhiteConfig.whiteList()).contains(uri)){
                // 如果是白名单直接放行
                filterChain.doFilter(request,response);
            } else {
                String token = request.getHeader("Auth-Token");
                if (Objects.isNull(token) || token.isEmpty()){
                    // Token不存在,拦截返回
                    authExeHandler.commence(request,response,null);
                } else {
                    Object object = authTokenService.getToken(token);
                    if (!Objects.isNull(object) && object instanceof User user){
                        UsernamePasswordAuthenticationToken authentication =
                                new UsernamePasswordAuthenticationToken(user, null,user.getAuthorities());
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                        filterChain.doFilter(request,response);
                    } else {
                        // Token验证失败,拦截返回
                        authExeHandler.commence(request,response,null);
                    }
                }
            }
        }
    }
    

    四、核心功能

    1、登录退出

    自定义登录退出两个接口,基于用户名和密码执行上述的身份认证流程,如果认证成功则返回用户的身份令牌,在请求「非」白名单接口时需要在请求头中Auth-Token:token携带该令牌,在退出时会清除身份信息;

    @Service
    public class LoginService {
    
        private static final Logger log = LoggerFactory.getLogger(LoginService.class);
    
        @Resource
        private AuthTokenService authTokenService ;
        @Resource
        private AuthenticationManager authenticationManager;
    
        public String doLogin (UserBase userBase){
            AbstractAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                    userBase.getUserName().trim(), userBase.getPassWord().trim());
            Authentication authentication = authenticationManager.authenticate(authToken) ;
            User user = (User) authentication.getDetails();
            return authTokenService.createToken(user) ;
        }
    
        public Boolean doLogout (String authToken){
            SecurityContextHolder.clearContext();
            return authTokenService.deleteToken(authToken) ;
        }
    }
    
    @Service
    public class AuthTokenService {
    
        private static final Logger log = LoggerFactory.getLogger(AuthTokenService.class);
        @Resource
        private RedisTemplate redisTemplate ;
    
        public String createToken (User user){
            String userName = user.getUsername();
            String token = DigestUtils.md5DigestAsHex(userName.getBytes());
            log.info("user-name:{},create-token:{}",userName,token);
            redisTemplate.opsForValue().set(token,user,10, TimeUnit.MINUTES);
            return token ;
        }
    
        public Object getToken (String token){
            return redisTemplate.opsForValue().get(token);
        }
    
        public Boolean deleteToken (String token){
            return redisTemplate.delete(token);
        }
    }
    

    2、权限校验

    UserWeb类中提供用户的注册接口,在用户表中创建两个测试用户:admin对应ROLE_Admin角色,user对应ROLE_User角色,验证如下几个接口的权限控制;

    select接口不需要鉴权,拦截器放行即可访问;getUser接口校验ROLE_User角色;getAdmin接口校验ROLE_Admin角色;query接口校验两个角色中的任意一个即可;

    两个不同用户登录获取到各自的身份令牌,使用不同的令牌请求接口,在PreAuthorize验证通过后才可以正常访问;

    @RestController
    public class UserWeb {
    
        @Resource
        private UserService userService ;
    
        @PostMapping("/register")
        public String register (@RequestBody UserBase userBase){
            return "register-"+userService.register(userBase) ;
        }
    
        @GetMapping("/select/{id}")
        public UserBase select (@PathVariable Integer id){
            return userService.getById(id) ;
        }
    
        @PreAuthorize("hasRole('User')")
        @GetMapping("/user/{id}")
        public UserBase getUser (@PathVariable Integer id){
            return userService.getById(id) ;
        }
    
        @PreAuthorize("hasRole('Admin')")
        @GetMapping("/admin/{id}")
        public UserBase getAdmin (@PathVariable Integer id){
            return userService.getById(id) ;
        }
    
        @PreAuthorize("hasAnyRole('User','Admin')")
        @GetMapping("/query/{id}")
        public UserBase query (@PathVariable Integer id){
            return userService.getById(id) ;
        }
    }
    

    五、参考源码

    文档仓库:
    https://gitee.com/cicadasmile/butte-java-note
    
    源码仓库:
    https://gitee.com/cicadasmile/butte-spring-parent
    
  • 相关阅读:
    K8S:Pod
    微服务框架 SpringCloud微服务架构 17 初识ES 17.3 ES 与MySQL 的概念对比
    CAS与自旋锁、ABA问题
    【算法与数据结构】--高级算法和数据结构--高级数据结构
    构建一个WIFI室内定位系统
    Linux-内存文件
    js常见算法及算法思想-分而治之
    eclispe项目中静态文件出现错误解决方法
    HDLbits exercises 2 (MODULES节选题)
    Linux HTTP协议
  • 原文地址:https://www.cnblogs.com/cicada-smile/p/17627749.html