SpringBoot3安全管理

標籤:Security.登錄.權限;

一、簡介

SpringSecurity組件可以爲服務提供安全管理的能力,比如身份驗證、授權和針對常見攻擊的保護,是保護基於spring應用程序的事實上的標準;

在實際開發中,最常用的是登錄驗證和權限體系兩大功能,在登錄時完成身份的驗證,加載相關信息和角色權限,在訪問其他系統資源時,進行權限的驗證,保護系統的安全;

二、工程搭建

1、工程結構

2、依賴管理

starter-security依賴中,實際上是依賴spring-security組件的6.1.1版本,對於該框架的使用,主要是通過自定義配置類進行控制;

<!-- 安全組件 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <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<GrantedAuthority> 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<UserBase> 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<String,Object> 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章