【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、重寫元件等方法,來對框架進行自定義
由於SpringSecurity
是spring
生態中重要的一員,不斷隨著版本更新維護而越來越完善和強大
SpringSecurity
提供了安全策略設定,進而對全域性的請求進行攔截和過濾,保證專案的安全性
SpringSecurity
也可以設定諸如cors、crsf等,可以自定義,但預設關閉,需要開啟否則會遮蔽設定給springMVC的相同的設定
4.SpringSecurity與shiro、jwt
SpringSecurity
與shiro
和jwt
相比之下,更顯得完善和強大,一方面能夠給開發者更大的自由發揮能力,開發出更符合業務需求的安全框架,但另一方面也略顯臃腫
shiro
相當於SpringSecurity
的精簡版,基本沿用了主體結構,並加以精簡和優化,使得使用起來更加方便
jwt
由於其特性,更加輕便和簡潔,但能力也更弱,單由jwt
只能實現簡單的許可權校驗,不適合用於較大的框架(能力不夠&安全不夠),因此往往與shiro
、SpringSecurity
等框架進行組合,共同協作來進行優勢互補
後記
相比之下。。我更願意用shiro擺平一切。。
作者:Echo_Ye
WX:Echo_YeZ
Email :[email protected]
個人站點:在搭了在搭了。。。(右鍵 - 新建資料夾)