基本功能(認證+授權)
https://blog.csdn.net/Lammonpeter/article/details/79611439
https://www.bilibili.com/video/av40943281
自定義用戶信息
在框架中的一個約定:以ROLE_開頭的爲角色。不以ROLE_開頭的爲權限。
UserDetails接口實現:自定義用戶信息類。保存用戶信息、角色+權限等。
UserDetailsService接口實現:自定義用戶信息提取類,框架登錄認證時通過此類完成認證,生成UserDetails存入session。
核心過濾器鏈
https://blog.csdn.net/dushiwodecuo/article/details/78913113
l流程 :SecurityContext裝配<——>認證登錄< ——>異常<——>鑑權<——>Mvc(dispatchServlet)
圖中爲過濾器鏈流程中的一些核心過濾器,請求線程chain.doFilter()方法向下調用過濾器。整個過程是同一個線程的方法棧,後進先出。圖中請求線是進棧,響應線是出棧。
第一個橙色的過濾器是請求進入時根據SessionID檢查Session(本地/分佈式redis等)中是否已存在SecurityContext,若存在則放入SecurityContextHolder(ThreadLocal)中作爲線程變量。響應返回方法棧退出時,他是最後一道通過,會清除SecurityContextHolder,將SecurityContext存放到Session中。保證不同請求線程能根據SessionID從Session中取得對應用戶的SecurityContext。【注意:只有這個過濾器退出時存放session】
其中綠色爲認證過濾器。第一個綠色爲不同認證功能對應的不同過濾器(根據不同的登錄方式選擇不同的過濾器),最後一個綠色是所有請求都會經過的匿名過濾器。
匿名過濾器最後檢查SecurityContextHolder.getContext().getAuthentication()==null,若是真則當前線程在前面的認證過濾器沒有從持久層或是sesson中得到用戶信息,匿名過濾器會統一爲當前線程添加一個匿名Authentication到SecurityContextHolder。
最後一個FilterSecurityInterceptor,是所有請求都會經過的最後一個鑑權過濾器,他是鑑權的核心實現。通過它就會訪問到controller,不通過會拋出異常給藍色的異常過濾器處理。
因爲鏈上的都是過濾器,所以Security在dispatchServlet之前執行。既在攔截器+AOP之前。
登陸後取不到Authentication
1、Authentication加載到ThreadLocal中,如果異步執行就無法取到Authentication。
2、請求沒有經過SecurityContextPersistenceFilter。這樣請求進入時Authentication不會加載到ThreadLocal,響應返回時也不會寫入session中。最好檢查URL是不是被錯誤的配置在ignore中,ignore是不走過濾器鏈的URL。可以把URL配置爲可以匿名訪問,這樣請求是通過過濾器鏈的,而不是ignore直接忽略。ignore適合配置靜態資源。
SecurityContextHolder
通過SecurityContextHolder取得Session中當前線程對應用戶的信息(UserDetails實現類),底層是ThreadLocal。
Session->Authentication ->UserDetails
可以自定義MySysUser類繼承org.springframework.security.core.userdetails.User,其實現了org.springframework.security.core.userdetails.UserDetails接口。自定義MySysUser類中可以存放自定義信息。
方法一:SecurityContextHolder
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User principal =
(org.springframework.security.core.userdetails.User) authentication.getPrincipal();
return principal.getUsername();
方法二:Spring自動注入
@RequestMapping("/url")
public String echo2(Authentication authentication) {
}
@RequestMapping("/url2")
public String echo(@AuthenticationPrincipal UserDetails user) {
}
下圖 用戶是通過用戶名密碼的方式登陸的,所以Authentication是UsernamePasswordAuthenticationToken類型。
下圖是系統中SecurityContextHolder.getContext()中保存的用戶信息UserDetails。authorities中保存用戶的(角色+權限)列表。在框架中的一個約定:以ROLE_開頭的爲角色。不以ROLE_開頭的爲權限。hasAuthority(權限)與hasRole(角色)分別是用來鑑定權限/角色的api。
認證流程
驗證登錄信息,創建用戶Authentication,放入SecurityContextHolder,最終將SecurityContext存入Session中。
之後請求直接從Session中取出SecurityContext。
FilterSecurityInterceptor鑑權過濾器
用戶權限信息+URL權限信息+決策器實現 鑑權工作
FilterSecurityInterceptor中核心驗證方法
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
//通過Request中的屬性,判斷是否已經經過此過濾器,是則放行
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
//首次進入 在Request添加屬性
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//beforeInvocation鑑權 若鑑權失敗 拋異常
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
beforeInvocation方法是鑑權的核心(URL權限緩存+決策器+用戶信息)
其通過securityMetadataSource.getAttributes()讀取url對應的權限,將(用戶信息+ request+url權限)傳入 accessDecisionManager.decide(authenticated, object, attributes)方法進行決策。
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
//SecurityMetadataSource取得URL對應權限
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
if (debug) {
logger.debug("Public object - authentication not attempted");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (debug) {
logger.debug("Secure object: " + object + "; Attributes: " + attributes);
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
//accessDecisionManager決策器通過(用戶信息+ request+url權限)鑑權
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
if (debug) {
logger.debug("Authorization successful");
}
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
if (debug) {
logger.debug("RunAsManager did not change Authentication object");
}
// no further work post-invocation
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
if (debug) {
logger.debug("Switching to RunAs Authentication: " + runAs);
}
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
// need to revert to token.Authenticated post-invocation
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
Spring Security 動態加載URL權限
自定義FilterSecurityInterceptor(鑑權過濾器):繼承AbstractSecurityInterceptor,使用自定義的securityMetadataSource+accessDecisionManager。調用super.beforeInvocation進行鑑權。
自定義securityMetadataSource(URL權限緩存):實現FilterInvocationSecurityMetadataSource接口,
自定義數據結構保存URL權限SecurityConfig,覆蓋實現getAttributes()讀取url對應的權限,爲決策器提供URL權限。
自定義accessDecisionManager(決策器):實現AccessDecisionManager接口,實現decide(authenticated, object, attributes)方法通過(用戶信息+ request+url權限)進行匹配決策。
https://blog.csdn.net/shanchahua123456/article/details/88949064
簡單用例
SpringSecurity動態修改用戶權限
每個用戶都有自己的Authentication,其保存在SecurityContextHolder中。Authentication是通過SpringSecurity的UserDetial實現填充信息。
@GetMapping("/vip/test")
@Secured("ROLE_VIP") // 需要ROLE_VIP權限可訪問
public String vipPath() {
return "僅 ROLE_VIP 可看";
}
@GetMapping("/vip")
public boolean updateToVIP() {
// 得到當前的認證信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 生成當前的所有授權
List<GrantedAuthority> updatedAuthorities = new ArrayList<>(auth.getAuthorities());
// 添加 ROLE_VIP 授權
updatedAuthorities.add(new SimpleGrantedAuthority("ROLE_VIP"));
// 生成新的認證信息
Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities);
// 重置認證信息
SecurityContextHolder.getContext().setAuthentication(newAuth);
return true;
}
假設當前你的權限只有 ROLE_USER。那麼按照上面的代碼:
1、直接訪問 /vip/test 路徑將會得到403的Response;
2、訪問 /vip 獲取 ROLE_VIP 授權,再訪問 /vip/test 即可得到正確的Response。
轉自http://www.spring4all.com/article/155
OncePerRequestFilter 與 GenericFilterBean
OncePerRequestFilter: https://blog.csdn.net/f641385712/article/details/87793736
自定義Security過濾器
HttpSessionRequestCache
配置細化
授權表達式放在antMatchers(URL)之後
1 對GET請求,URL="/user/{id}"權限攔截
authorizeRequests().antMatchers(HttpMethod.GET,"/user/*").hasRole("ADMIN")
2 通過hasRole方法底層源碼可以看到最終拼接的角色表達式是"hasRole('ROLE_ADMIN')"。所以用戶角色對應的是ROLE_ADMIN。
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
} else {
return "hasRole('ROLE_" + role + "')";
}
}
3 hasAuthority(權限)與hasRole(角色)不同,其實完全匹配,hasRole是自動加ROLE,因爲hasRole被認定爲角色。
在框架中的一個約定:以ROLE開頭的爲角色。不以ROLE開頭的爲權限。
比如:
hasAuthority("read") 用戶需要"read"權限
hasRole("read") 用戶需要"ROLE_read"角色
4 符合配置
需要ADMIN角色和read權限
authorizeRequests().antMatchers("/user/*").access("hasRole('ADMIN') and hasAuthority('read') ")
spring session redis+security 相同用戶單個session的解決方案
https://www.e-learn.cn/index.php/content/redis/730910
在springsecurity配置中,註冊spring session redis 的sessionregistry。
自定義AccessDeniedHandler
https://blog.csdn.net/tjyyyangyi/article/details/79413548
處理被springsecurity拒絕的請求。只有確實的訪問失敗纔會進入AccessDeniedHandler,如果是未登陸或者會話超時等,不會觸發AccessDeniedHandler,而是會直接跳轉到登陸頁面
SpringSecurity限制iframe引用頁面。出現X-Frame-Options deny問題
https://blog.csdn.net/u014643282/article/details/81131092
出現這個問題的原因是因爲Spring Security默認將header response裏的X-Frame-Options屬性設置爲DENY。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/res/**", "/admin", "/thirdparty/**", "/auth/login").permitAll()
.antMatchers("/admin/**").hasAuthority("admin:index")
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/admin").permitAll()
.and()
.logout().logoutUrl("/admin/logout").logoutSuccessUrl("/admin").invalidateHttpSession(true)
.and()
.csrf().disable()
.headers().frameOptions().sameOrigin();
}