1. 程式人生 > 實用技巧 >【SpringSecurity】初識與整合

【SpringSecurity】初識與整合

個人學習筆記分享,當前能力有限,請勿貶低,菜鳥互學,大佬繞道

如有勘誤,歡迎指出和討論,本文後期也會進行修正和補充


前言

之前介紹過Shiro,作為SpringSecurity的精簡版,已經具備了大部分常用功能,且更加便於使用,因而一定程度上成為了SpringSecurity的替代品。

相比之下,SpringSecurity功能更加強大完善,通過調整和組合其中的元件,能得到一個高度自定義的安全框架。

本文中,將基於SpringSecurity+Jwt進行學習。


1.介紹

1.1.簡介

SpringSecurity是一個能夠為基於Spring的企業應用系統提供宣告式的安全訪問控制解決方案的安全框架。

由於它是Spring生態系統中的一員,因此它伴隨著整個Spring生態系統不斷修正、升級,在Spring Boot專案中加入SpringSecurity更是十分簡單,使用SpringSecurity 減少了為企業系統安全控制編寫大量重複程式碼的工作。


1.1.1.SpringSecurity主要包括兩個目標

  • 認證(Authentication):建立一個他宣告的主題,即確認使用者可以訪問當前系統。
  • 授權(Authorization):確定一個主體是否允許在你的應用程式執行一個動作,即確定使用者在當前系統下所有的功能許可權。

Shiro及其他很多安全框架也以此為目標


1.1.2.SpringSecurity的認證模式

在身份驗證層,Spring Security 的支援多種認證模式。

這些驗證絕大多數都是要麼由第三方提供,或由相關的標準組織,如網際網路工程任務組開發。

另外Spring Security 提供自己的一組認證功能,內容過長,此處不再贅述,有興趣自己瞭解。


1.1.3.核心元件

  • SecurityContextHolder:提供對SecurityContext的訪問
  • SecurityContext,:持有Authentication物件和其他可能需要的資訊
  • AuthenticationManager:其中可以包含多個AuthenticationProvider
  • ProviderManager
    :物件為AuthenticationManager介面的實現類
  • AuthenticationProvider:主要用來進行認證操作的類 呼叫其中的authenticate()方法去進行認證操作
  • Authentication:Spring Security方式的認證主體
  • GrantedAuthority:對認證主題的應用層面的授權,含當前使用者的許可權資訊,通常使用角色表示
  • UserDetails:構建Authentication物件必須的資訊,可以自定義,可能需要訪問DB得到
  • UserDetailsService:通過username構建UserDetails物件,通過loadUserByUsername根據userName獲取UserDetail物件

1.1.4.常見過濾器

  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • LogoutFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • UsernamePasswordAuthenticationFilter
  • BasicAuthenticationFilter

通常可以繼承並重寫過濾器,已滿足業務要求


2.整合

2.1.依賴

    // Spring security
    implementation 'org.springframework.boot:spring-boot-starter-security:2.3.5.RELEASE'
    // JWT
    implementation 'com.auth0:java-jwt:3.11.0'

2.2.準備元件

僅列出清單,不做贅述,有興趣可以直接看demo

  • BaseResponse:統一返回結果
  • DataResponse:統一返回資料規範
  • BaseExceptionHandler:異常統一捕獲處理器

2.3.核心元件

2.3.1.配置安全中心(核心)

package com.demo.security;

import com.demo.handler.AuthExceptionHandler;
import com.demo.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @Description: web安全配置
 * @Author: Echo
 * @Time: 2020/12/8 11:04
 * @Email: [email protected]
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    /**
     * cors跨域配置
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }

    /**
     * @description: WebMvc配置
     * @author: Echo
     * @email: [email protected]
     * @time: 2020/12/8 11:12
     */
    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        /**
         * 註冊cors
         */
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            // 允許路徑common下的跨域
            registry.addMapping("/common/**")   // 允許路徑
                    .allowCredentials(true) // 不使用cookie故關閉認證
                    .allowedOrigins("*")    // 允許源,設定為全部
                    .allowedMethods("*")    // 允許方法,設定為全部
                    .allowedHeaders("*")    // 允許頭,設定為全部
                    .maxAge(3600)   // 快取時間,設定為1小時
            ;
        }
    }

    /**
     * @description: SpringSecurity配置
     * @author: Echo
     * @email: [email protected]
     * @time: 2020/12/8 10:15
     */
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        private final UserService userService;

        WebSecurityConfig(UserService userService) {
            this.userService = userService;
        }

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // 設定service,需要實現方法loadUserByUsername,用於登入
            auth.userDetailsService(userService);
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.cors()     // 開啟跨域
                    .and()
                    .csrf().disable()   // 禁用csrf
                    .antMatcher("/**").authorizeRequests()    // 訪問攔截
                    .antMatchers("/auth/**").permitAll()  // auth路徑下訪問放行
                    .antMatchers("/admin/**").hasRole(AccessConstants.ROLE_ADMIN)    // admin路徑下限制訪問角色
                    .antMatchers("/user/**").hasRole(AccessConstants.ROLE_USER)    // user路徑下限制訪問角色
                    .anyRequest().authenticated()   // 身份驗證
                    .and()
                    .addFilter(new JwtTokenFilter(authenticationManager(), userService))   // 自定義過濾器-jwt
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)     // session策略-永不
                    .and()
                    .exceptionHandling()
                    .authenticationEntryPoint(new AuthExceptionHandler())   // 異常處理-認證失敗
                    .accessDeniedHandler(new AuthExceptionHandler());   //異常處理-許可權錯誤
        }
    }
}

核心功能為重寫WebSecurityConfigurerAdapter,設定安全配置

  • 重寫configure(AuthenticationManagerBuilder auth),設定service,其中service必須繼承類UserDetailsService,並需要重寫loadUserByUsername方法,用於解析token並組裝使用者資訊
  • 重寫configure(HttpSecurity http),設定相關安全策略,並設定一個過濾器

本配置中還配置了cors跨域,開放了路徑/common/**下的跨域,請按照實際需求設定

這裡還可以設定很多東西,這裡只介紹一小部分


2.3.2.自定義token(基於Jwt)

package com.demo.security;

import lombok.Getter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @description: 自定義token
 * @author: Echo
 * @email: [email protected]
 * @time: 2020/12/8 10:05
 */
public class JwtToken extends UsernamePasswordAuthenticationToken implements UserDetails {
    /**
     * token包括的資訊
     */
    @Getter
    private final Type type;
    @Getter
    private final Long userId;

    /**
     * token的claim包括的常量key
     */
    public static final String CLAIM_KEY_USER_ID = "userId";
    public static final String CLAIM_KEY_MOBILE = "mobile";
    public static final String CLAIM_KEY_TYPE = "type";
    public static final String CLAIM_KEY_RULES = "rules";
    public static final int TOKEN_EXPIRES_DAYS = 1;

    public JwtToken(Type type, Object principal, Long userId, Collection<? extends GrantedAuthority> authorities) {
        // credentials設定為空,身份驗證不由jwt管理
        super(principal, null, authorities);
        this.type = type;
        this.userId = userId;
    }

    // 列舉type
    public enum Type {
        ADMIN(),
        USER(),
        ;
    }

    // 棄用密碼,不使用
    @Override
    public String getPassword() {
        return null;
    }

    // 獲取賬號
    @Override
    public String getUsername() {
        return String.valueOf(this.getPrincipal());
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}

繼承了類UsernamePasswordAuthenticationToken並實現介面UserDetails,用於儲存身份資訊

重寫了從主題中讀取賬號和密碼的功能,因為密碼不參與儲存故直接返回空,賬號則直接型別轉換獲取


2.3.3.token過濾器

package com.demo.security;

import com.google.common.base.Strings;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

/**
 * @description: token過濾器,提供給SpringSecurity使用
 * @author: Echo
 * @email: [email protected]
 * @time: 2020/12/8 10:07
 */
public class JwtTokenFilter extends BasicAuthenticationFilter {
    public static final String TOKEN_HEADER = "Token";
    public static final String TOKEN_SECRET = "e8258f17-b436-4cad-bfc3-2d810ec86238";

    private final UserDetailsService userDetailsService;

    public JwtTokenFilter(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
        super(authenticationManager);
        this.userDetailsService = userDetailsService;
    }

    /**
     * AOP,將token中的資料,提取出來放入request作為引數
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader(TOKEN_HEADER);
        if (!Strings.isNullOrEmpty(token)) {
            JwtToken details = (JwtToken) userDetailsService.loadUserByUsername(token);
            if (Objects.nonNull(details)) {
                SecurityContextHolder.getContext().setAuthentication(details);
                request.setAttribute("authId", details.getUserId());
                request.setAttribute("authType", details.getType());
                request.setAttribute("auth", details);
            }
        }
        chain.doFilter(request, response);
    }
}

需要繼承類BasicAuthenticationFilter,並重寫其內部過濾方法,通過AOP的方式將token中的資料寫入request

之前介紹的純Jwt認證框架也是使用的AOP來校驗token引數和寫入引數到request


2.3.4.認證異常handler

package com.demo.handler;

import com.demo.response.BaseResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @description: 認證異常handler
 * @author: Echo
 * @email: [email protected]
 * @time: 2020/12/8 9:40
 */
@Slf4j
public class AuthExceptionHandler implements AccessDeniedHandler, AuthenticationEntryPoint {
    private final ObjectWriter objectWriter = new ObjectMapper().writer().withDefaultPrettyPrinter();

    /**
     * 禁止訪問
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        sendError(response, false, accessDeniedException.getLocalizedMessage());
    }

    /**
     * 未登入
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        sendError(response, true, authException.getLocalizedMessage());
    }

    private void sendError(HttpServletResponse response, boolean redirectLogin, String message) throws IOException {
        response.setCharacterEncoding(Charsets.UTF_8.displayName());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        log.error("許可權錯誤", message);
        objectWriter.writeValue(response.getWriter(), redirectLogin ? BaseResponse.RESPONSE_NOT_LOGIN : BaseResponse.RESPONSE_AUTH_DENIED);
    }
}

其實就是同時繼承許可權拒絕回撥和認證拒絕回撥,進行統一處理,分開寫也是可以的,寫一起只是為了省事。。。


2.3.5.許可權鑑別器(核心)

package com.demo.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiPredicate;

/**
 * @description: 許可權鑑別器
 * @author: Echo
 * @email: [email protected]
 * @time: 2020/12/8 9:28
 */
@Slf4j
@Component
public class GlobalPermissionEvaluator implements PermissionEvaluator {
    /**
     * 自定義斷言
     */
    public interface EvalPredicate extends BiPredicate<Object, JwtToken> {

    }

    /**
     * 許可權map,用於儲存相關許可權
     */
    private final Map<Object, EvalPredicate> userPredicates = new HashMap<>();
    private final Map<Object, EvalPredicate> adminPredicates = new HashMap<>();

    /**
     * @description: 檢查許可權
     * @author: Echo
     * @email: [email protected]
     * @time: 2020/12/8 9:29
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
        if (!(authentication instanceof JwtToken)) {
            return false;
        }
        // 轉換為可識別的 userDetails
        JwtToken userDetails = (JwtToken) authentication;
        Map<Object, EvalPredicate> predicates = getPredicates(userDetails.getType());
        // 檢查許可權
        boolean pass = false;
        if (predicates.containsKey(permission)) {
            pass = predicates.get(permission).test(targetDomainObject, userDetails);
        } else {
            log.info("reject permission: {} {}", permission, targetDomainObject);
        }
        return pass;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return hasPermission(authentication, targetId, permission);
    }

    /**
     * @description: 許可權註冊
     * @author: Echo
     * @email: [email protected]
     * @time: 2020/12/8 9:38
     */
    public void registerPermission(JwtToken.Type type, Object permission, EvalPredicate predicate) {
        Map<Object, EvalPredicate> predicates = getPredicates(type);
        if (predicates.containsKey(permission)) {
            throw new DuplicateKeyException("Permission handler duplicate");
        }
        predicates.put(permission, predicate);
    }

    /**
     * @description: 獲取許可權
     * @author: Echo
     * @email: [email protected]
     * @time: 2020/12/8 9:38
     */
    private Map<Object, EvalPredicate> getPredicates(JwtToken.Type type) {
        // 獲取集合
        Map<Object, EvalPredicate> predicates;
        switch (type) {
            case USER -> predicates = this.userPredicates;
            case ADMIN -> predicates = this.adminPredicates;
            default -> throw new SecurityException("Permission type not supported!");
        }
        return predicates;

    }
}

需要實現介面PermissionEvaluator,實現其許可權認證邏輯

採用方案如下

  • 鑑別器中定義map,用於儲存每個型別的許可權和對應的斷言
  • service中註冊時,將所需要的註冊的許可權型別和對應的斷言,註冊到鑑別器中
  • 認證許可權時,會呼叫相對應的斷言,進行許可權測試

2.3.6.service層

package com.demo.service;

import com.demo.security.GlobalPermissionEvaluator;

/**
 * @Description:許可權service介面層
 * @Author: Echo
 * @Time: 2020/12/8 16:55
 * @Email: [email protected]
 */
public interface AccessService {
    /**
     * 註冊許可權資訊
     */
    void initEvaluator(GlobalPermissionEvaluator evaluator);

    /**
     * 校驗許可權資訊
     */
    boolean isAccessible(Long userId, Long targetId);
}
package com.demo.service;

import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * @description: 使用者service介面層
 * @author: Echo
 * @email: [email protected]
 * @time: 2020/12/8 10:16
 */
public interface UserService{

    /**
     * 登入
     *
     * @param username 賬號
     * @param password 密碼
     * @return token
     */
    String login(String username, String password);
}
package com.demo.service.impl;


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.demo.security.AccessConstants;
import com.demo.security.GlobalPermissionEvaluator;
import com.demo.security.JwtToken;
import com.demo.security.JwtTokenFilter;
import com.demo.service.AccessService;
import com.demo.service.UserService;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Calendar;
import java.util.Date;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @description: 使用者service實現層
 * @author: Echo
 * @email: [email protected]
 * @time: 2020/12/8 10:16
 */
@Service
public class UserServiceImpl implements UserService, AccessService {
    private final Algorithm jwtAlgorithm;
    private final JWTVerifier jwtVerifier;

    public UserServiceImpl(GlobalPermissionEvaluator evaluator) {
        this.jwtAlgorithm = Algorithm.HMAC256(JwtTokenFilter.TOKEN_SECRET.getBytes());
        this.jwtVerifier = JWT.require(jwtAlgorithm).build();
        initEvaluator(evaluator);
    }

    /**
     * 註冊許可權資訊
     */
    @Override
    public void initEvaluator(GlobalPermissionEvaluator evaluator) {
        // todo 按照實際情況註冊許可權
        // 註冊管理員的更新許可權
        evaluator.registerPermission(JwtToken.Type.ADMIN, AccessConstants.ACCESS_UPDATE, (targetId, token) ->
                this.isAccessible(token.getUserId(), (Long) targetId));
        // 註冊使用者的更新許可權
        evaluator.registerPermission(JwtToken.Type.USER, AccessConstants.ACCESS_UPDATE, (targetId, token) ->
                this.isAccessible(token.getUserId(), (Long) targetId));
    }

    /**
     * 校驗許可權資訊
     */
    @Override
    public boolean isAccessible(Long userId, Long targetId) {
        // todo 從資料庫查詢校驗許可權
        Set<Long> accessIds = switch (userId.toString()) {
            case "1" -> Sets.newHashSet(1L, 2L, 3L, 4L);
            case "2" -> Sets.newHashSet(1L, 2L, 3L);
            case "3" -> Sets.newHashSet(1L, 2L);
            case "4" -> Sets.newHashSet(1L);
            default -> Sets.newHashSet();
        };
        return accessIds.contains(targetId);
    }

    /**
     * 裝載token
     */
    @Override
    public UserDetails loadUserByUsername(String token) throws UsernameNotFoundException {
        try {
            //解析token
            DecodedJWT verify = jwtVerifier.verify(token);
            Long userId = verify.getClaim(JwtToken.CLAIM_KEY_USER_ID).asLong();
            String mobile = verify.getClaim(JwtToken.CLAIM_KEY_MOBILE).asString();
            JwtToken.Type type = verify.getClaim(JwtToken.CLAIM_KEY_TYPE).as(JwtToken.Type.class);
            String rules = Strings.nullToEmpty(verify.getClaim(JwtToken.CLAIM_KEY_RULES).asString());
            Preconditions.checkNotNull(userId);
            Preconditions.checkState(!Strings.isNullOrEmpty(mobile));
            // 獲取許可權
            //noinspection UnstableApiUsage
            Set<GrantedAuthority> authorities = Splitter.on("|")
                    .splitToStream(rules)
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toSet());
            //組裝token todo這裡
            return new JwtToken(type, mobile, userId, authorities);
        } catch (Throwable e) {
            throw new UsernameNotFoundException(e.getMessage());
        }
    }

    /**
     * 登入
     *
     * @param username 賬號
     * @param password 密碼
     * @return token
     */
    @Override
    public String login(String username, String password) {
        // 相關引數 todo 正常業務中需要從資料庫中查詢,這裡也未校驗密碼
        Long userId = null;
        String mobile = null;
        String rules = null;
        JwtToken.Type type = null;
        switch (username) {
            case "111":
                userId = 1L;
                mobile = "13411111111";
                rules = AccessConstants.formatAccess(
                        AccessConstants.ACCESS_FIND,
                        AccessConstants.ACCESS_UPDATE,
                        AccessConstants.ACCESS_DELETE,
                        AccessConstants.ACCESS_INSERT,
                        AccessConstants.ROLE_HEAD + AccessConstants.ROLE_ADMIN
                );
                type = JwtToken.Type.ADMIN;
                break;
            case "222":
                userId = 2L;
                mobile = "13422222222";
                rules = AccessConstants.formatAccess(
                        AccessConstants.ACCESS_FIND,
                        AccessConstants.ACCESS_UPDATE,
                        AccessConstants.ROLE_ADMIN,
                        AccessConstants.ROLE_HEAD + AccessConstants.ROLE_ADMIN
                );
                type = JwtToken.Type.ADMIN;
                break;
            case "333":
                userId = 3L;
                mobile = "13433333333";
                rules = AccessConstants.formatAccess(
                        AccessConstants.ACCESS_FIND,
                        AccessConstants.ACCESS_INSERT,
                        AccessConstants.ROLE_HEAD + AccessConstants.ROLE_USER
                );
                type = JwtToken.Type.USER;
                break;
            case "444":
                userId = 4L;
                mobile = "1344444444";
                rules = AccessConstants.formatAccess(
                        AccessConstants.ACCESS_FIND,
                        AccessConstants.ROLE_HEAD + AccessConstants.ROLE_USER
                );
                type = JwtToken.Type.USER;
                break;
            default:
                throw new RuntimeException("賬號或密碼不正確");
        }
        // 過期時間為1天
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.DATE, JwtToken.TOKEN_EXPIRES_DAYS);

        //組裝token
        return JWT.create()
                .withClaim(JwtToken.CLAIM_KEY_USER_ID, userId)
                .withClaim(JwtToken.CLAIM_KEY_MOBILE, mobile)
                .withClaim(JwtToken.CLAIM_KEY_RULES, rules)
                .withClaim(JwtToken.CLAIM_KEY_TYPE, type.toString())
                .withIssuer(this.getClass().getSimpleName())
                .withIssuedAt(new Date())
                .withExpiresAt(calendar.getTime())
                .sign(jwtAlgorithm);
    }
}

分三個功能

  • 實現登入邏輯供controller層介面使用,通過賬號密碼查詢相關資訊並生成token

    為作為controller呼叫的業務層負責的內容,與框架本身無關

  • 實現loadUserByUsername(String token)方法供安全中心使用,通過token解析出相關資料,並轉換為UserDetails物件

    為安全中心需要的功能,如果有多個身份驗證方法也可以多個類實現該方法,並在安全中心選擇合適的進行配置

  • 實現註冊許可權資訊方法和許可權校驗方法,並在初始化的時候進行註冊,許可權校驗(hasPermission)時呼叫校驗

    為許可權校驗相關方法,service初始化時進行註冊許可權並存儲校驗方法,所有的許可權都應該在合適的地方註冊並存儲許可權校驗方法


另一個service

package com.demo.service.impl;

import com.demo.security.AccessConstants;
import com.demo.security.GlobalPermissionEvaluator;
import com.demo.security.JwtToken;
import com.demo.service.AccessService;
import com.google.common.collect.Sets;
import org.springframework.stereotype.Service;

import java.util.Set;

/**
 * @Description:測試service,用於測試其他許可權
 * @Author: Echo
 * @Time: 2020/12/8 17:50
 * @Email: [email protected]
 */
@Service
public class TestServiceImpl implements AccessService {
    public TestServiceImpl(GlobalPermissionEvaluator evaluator) {
        initEvaluator(evaluator);
    }

    @Override
    public void initEvaluator(GlobalPermissionEvaluator evaluator) {
        // 註冊管理員的詳情許可權
        evaluator.registerPermission(JwtToken.Type.ADMIN, AccessConstants.ACCESS_DETAIL, (targetId, token) ->
                this.isAccessible(token.getUserId(), (Long) targetId));
    }

    @Override
    public boolean isAccessible(Long userId, Long targetId) {
        // todo 從資料庫查詢校驗許可權
        Set<Long> accessIds = switch (userId.toString()) {
            case "1" -> Sets.newHashSet(1L, 2L);
            case "2" -> Sets.newHashSet(1L);
            default -> Sets.newHashSet();
        };
        return accessIds.contains(targetId);
    }
}

2.4.測試controller

2.4.1.登入

package com.demo.controller;

import com.demo.service.UserService;
import com.demo.response.DataResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController("authController")
@RequestMapping("/auth")
public class LoginController {
    private final UserService userService;

    public LoginController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("login")
    public DataResponse<?> login(
            @RequestParam(name = "username") String username,
            @RequestParam(name = "password") String password) {
        return new DataResponse(userService.login(username, password));
    }
}

執行結果

2.4.2.admin許可權

package com.demo.controller;

import com.demo.response.DataResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController("adminTestController")
@RequestMapping("/admin/test")
public class AdminTestController {

    /**
     * 測試update許可權
     */
    @GetMapping("checkLogin")
    @PreAuthorize("hasAuthority(T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
    public DataResponse<?> checkLogin() {
        return new DataResponse("OK");
    }

    /**
     * 測試對目標的update許可權
     */
    @GetMapping("checkUpdatePermission")
    @PreAuthorize("hasAuthority(T(com.demo.security.AccessConstants).ACCESS_UPDATE)"
            + "&&hasPermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
    public DataResponse<?> checkUpdatePermission(Long targetId) {
        return new DataResponse("OK");
    }

    /**
     * 測試對目標的detail許可權
     */
    @GetMapping("checkDetailPermission")
    @PreAuthorize("hasPermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_DETAIL)")
    public DataResponse<?> checkDetailPermission(Long targetId) {
        return new DataResponse("OK");
    }
}

執行結果

token寫入headers裡面,管理員賬號均為111

2.4.3.user許可權

package com.demo.controller;

import com.demo.response.DataResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController("userTestController")
@RequestMapping("/user/test")
public class UserTestController {

    /**
     * 檢查登入狀態
     */
    @GetMapping("checkLogin")
    public DataResponse<?> checkLogin() {
        return new DataResponse("OK");
    }

    /**
     * 檢查update許可權
     */
    @GetMapping("checkPermission")
    @PreAuthorize("hasUpdatePermission(#targetId,T(com.demo.security.AccessConstants).ACCESS_UPDATE)")
    public DataResponse<?> checkUpdatePermission(Long targetId) {
        return new DataResponse("OK");
    }
}

使用之前賬號為111的token訪問(admin賬號)

換用賬號為444的token訪問

測試結果均符合預期


3.小結

SpringSecurity主要是用註解、aop、重寫元件等方法,來對框架進行自定義

由於SpringSecurityspring生態中重要的一員,不斷隨著版本更新維護而越來越完善和強大

SpringSecurity提供了安全策略設定,進而對全域性的請求進行攔截和過濾,保證專案的安全性

SpringSecurity也可以設定諸如cors、crsf等,可以自定義,但預設關閉,需要開啟否則會遮蔽設定給springMVC的相同的設定


4.SpringSecurity與shiro、jwt

SpringSecurityshirojwt相比之下,更顯得完善和強大,一方面能夠給開發者更大的自由發揮能力,開發出更符合業務需求的安全框架,但另一方面也略顯臃腫

shiro相當於SpringSecurity的精簡版,基本沿用了主體結構,並加以精簡和優化,使得使用起來更加方便

jwt由於其特性,更加輕便和簡潔,但能力也更弱,單由jwt只能實現簡單的許可權校驗,不適合用於較大的框架(能力不夠&安全不夠),因此往往與shiroSpringSecurity等框架進行組合,共同協作來進行優勢互補


後記

相比之下。。我更願意用shiro擺平一切。。


作者:Echo_Ye

WX:Echo_YeZ

Email :[email protected]

個人站點:在搭了在搭了。。。(右鍵 - 新建資料夾)