SpringBoot動(dòng)態(tài)權(quán)限校驗(yàn):從零到一實(shí)現(xiàn)高效、優(yōu)雅的解決方案
1、背景
- 實(shí)現(xiàn)自定義的登錄認(rèn)證。
- 登錄成功,生成token并將token 交由redis管理。
- 登錄后對(duì)用戶訪問的接口進(jìn)行接口級(jí)別權(quán)限認(rèn)證。
SpringSecurity提供的注解權(quán)限校驗(yàn)適合的場(chǎng)景是系統(tǒng)中僅有固定的幾個(gè)角色,且角色的憑證不可修改(如果修改需要改動(dòng)代碼)。
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
注:ROLE_TELLER是寫死的。
后端系統(tǒng)的訪問請(qǐng)求有以下幾種類型:
- 登錄、登出(可自定義url)
- 匿名用戶可訪問的接口(靜態(tài)資源,demo示例等)
- 其他接口(在登錄的前提下,繼續(xù)判斷訪問者是否有權(quán)限訪問)
2、環(huán)境搭建
<!--springSecurity安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
<!-- 默認(rèn)通過SESSIONId改為通過請(qǐng)求頭與redis配合驗(yàn)證session -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<!--redis支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
注:springBoot版本也是2.3.4.RELEASE,如果有版本對(duì)應(yīng)問題,自行解決。有用到swagger,為了便于測(cè)試。
新建springSecurity配置類
WebSecurityConfig作為springSecurity的主配置文件。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* Swagger等靜態(tài)資源不進(jìn)行攔截
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/error",
"/webjars/**",
"/resources/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置一些不需要登錄就可以訪問的接口
.antMatchers("/demo/**", "/about/**").permitAll()
//任何尚未匹配的URL只需要用戶進(jìn)行身份驗(yàn)證
.anyRequest().authenticated()
.and()
.formLogin()//允許用戶進(jìn)行基于表單的認(rèn)證
.loginPage("/mylogin");
}
}
注:證明可以訪問靜態(tài)資源不會(huì)被攔截
自定義登錄認(rèn)證
我們需要自定義:
- 登錄過濾器:負(fù)責(zé)過濾登錄請(qǐng)求,再交由自定義的登錄認(rèn)證管理器處理。
- 登錄成功處理類:顧名思義,登錄成功后的一些處理(設(shè)置返回信息提示“登錄成功!”,返回?cái)?shù)據(jù)類型為json)。
- 登錄失敗處理類:類似登錄成功處理類。ps:登錄成功處理類和失敗處理類有默認(rèn)的實(shí)現(xiàn)可以不自定義。但是建議自定義,因?yàn)榉祷氐男畔橛⑽?,一般情況不符合要求。
- 登錄認(rèn)證管理器:根據(jù)過濾器傳過來的登錄參數(shù),進(jìn)行登錄認(rèn)證,認(rèn)證后授權(quán)。
新建登錄成功處理類
需要實(shí)現(xiàn) AuthenticationSuccessHandler
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationSuccessHandler.class);
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
//登錄成功返回的認(rèn)證體,具體格式在后面的登錄認(rèn)證管理器中
String responseJson = JackJsonUtil.object2String(ResponseFactory.success(authentication));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("登錄成功!");
}
response.getWriter().write(responseJson);
}
}
新建登錄失敗處理類
實(shí)現(xiàn) AuthenticationFailureHandler
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
String errorMsg;
if (StringUtils.isNotBlank(e.getMessage())) {
errorMsg = e.getMessage();
} else {
errorMsg = CodeMsgEnum.LOG_IN_FAIL.getMsg();
}
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String responseJson = JackJsonUtil.object2String(ResponseFactory.fail(CodeMsgEnum.LOG_IN_FAIL,errorMsg));
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("認(rèn)證失敗!");
}
response.getWriter().write(responseJson);
}
}
新建登錄認(rèn)證管理器
實(shí)現(xiàn) AuthenticationProvider ,負(fù)責(zé)具體的身份認(rèn)證(一般數(shù)據(jù)庫(kù)認(rèn)證,在登錄過濾器過濾掉請(qǐng)求后傳入)
@Component
public class UserVerifyAuthenticationProvider implements AuthenticationProvider {
private PasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = (String) authentication.getPrincipal(); // Principal 主體,一般指用戶名
String passWord = (String) authentication.getCredentials(); //Credentials 網(wǎng)絡(luò)憑證,一般指密碼
//通過賬號(hào)去數(shù)據(jù)庫(kù)查詢用戶以及用戶擁有的角色信息
UserRoleVo userRoleVo = userService.findUserRoleByAccount(userName);
//數(shù)據(jù)庫(kù)密碼
String encodedPassword = userRoleVo.getPassWord();
//credentials憑證即為前端傳入密碼,因?yàn)榍岸艘话阌肂ase64加密過所以需要解密。
String credPassword = new String(Base64Utils.decodeFromString(passWord), StandardCharsets.UTF_8);
// 驗(yàn)證密碼:前端明文,數(shù)據(jù)庫(kù)密文
passwordEncoder = new MD5Util();
if (!passwordEncoder.matches(credPassword, encodedPassword)) {
throw new AuthenticationServiceException("賬號(hào)或密碼錯(cuò)誤!");
}
//ps:GrantedAuthority對(duì)認(rèn)證主題的應(yīng)用層面的授權(quán),含當(dāng)前用戶的權(quán)限信息,通常使用角色表示
List<GrantedAuthority> roles = new LinkedList<>();
List<Role> roleList = userRoleVo.getRoleList();
roleList.forEach(role -> {
SimpleGrantedAuthority roleId = new SimpleGrantedAuthority(role.getRoleId().toString());
roles.add(roleId);
});
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, passWord, roles);
token.setDetails(userRoleVo);//這里可以放用戶的詳細(xì)信息
return token;
}
@Override
public boolean supports(Class<?> authentication) {
return false;
}
}
新建登錄過濾器
LoginFilter.java繼承UsernamePasswordAuthenticationFilter,負(fù)責(zé)過濾登錄請(qǐng)求并交由登錄認(rèn)證管理器進(jìn)行具體的認(rèn)證。
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private UserVerifyAuthenticationProvider authenticationManager;
/**
* @param authenticationManager 認(rèn)證管理器
* @param successHandler 認(rèn)證成功處理類
* @param failureHandler 認(rèn)證失敗處理類
*/
public LoginFilter(UserVerifyAuthenticationProvider authenticationManager,
CustomAuthenticationSuccessHandler successHandler,
CustomAuthenticationFailureHandler failureHandler) {
//設(shè)置認(rèn)證管理器(對(duì)登錄請(qǐng)求進(jìn)行認(rèn)證和授權(quán))
this.authenticationManager = authenticationManager;
//設(shè)置認(rèn)證成功后的處理類
this.setAuthenticationSuccessHandler(successHandler);
//設(shè)置認(rèn)證失敗后的處理類
this.setAuthenticationFailureHandler(failureHandler);
//可以自定義登錄請(qǐng)求的url
super.setFilterProcessesUrl("/myLogin");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
//轉(zhuǎn)換請(qǐng)求入?yún)? UserDTO loginUser = new ObjectMapper().readValue(request.getInputStream(), UserDTO.class);
//入?yún)魅胝J(rèn)證管理器進(jìn)行認(rèn)證
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassWord())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
最后配置到WebSecurityConfig中:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserVerifyAuthenticationProvider authenticationManager;//認(rèn)證用戶類
@Autowired
private CustomAuthenticationSuccessHandler successHandler;//登錄認(rèn)證成功處理類
@Autowired
private CustomAuthenticationFailureHandler failureHandler;//登錄認(rèn)證失敗處理類
/**
* Swagger等靜態(tài)資源不進(jìn)行攔截
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/error",
"/webjars/**",
"/resources/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置一些不需要登錄就可以訪問的接口
.antMatchers("/demo/**", "/about/**").permitAll()
//任何尚未匹配的URL只需要用戶進(jìn)行身份驗(yàn)證
.anyRequest().authenticated()
.and()
//配置登錄過濾器
.addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
.csrf().disable();
}
}
驗(yàn)證配置
訪問登錄請(qǐng)求:
成功進(jìn)入LoginFilter
圖片
安全頭和登錄返回token
session:
store-type: redis
redis:
namespace: spring:session:admin
# session 無操作失效時(shí)間 30 分鐘
timeout: 1800
設(shè)置token放入返回的header中需要在WebSecurityConfig中加入
/**
* 配置 HttpSessionIdResolver Bean
* 登錄之后將會(huì)在 Response Header x-auth-token 中 返回當(dāng)前 sessionToken
* 將token存儲(chǔ)在前端 每次調(diào)用的時(shí)候 Request Header x-auth-token 帶上 sessionToken
*/
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
關(guān)于安全頭信息可以參考:
- https://docs.spring.io/spring-security/site/docs/5.2.1.BUILD-SNAPSHOT/reference/htmlsingle/#ns-headers
安全請(qǐng)求頭需要設(shè)置WebSecurityConfig中加入
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置一些不需要登錄就可以訪問的接口
.antMatchers("/demo/**", "/about/**").permitAll()
//任何尚未匹配的URL只需要用戶進(jìn)行身份驗(yàn)證
.anyRequest().authenticated()
.and()
//配置登錄過濾器
.addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
.csrf().disable();
//配置頭部
http.headers()
.contentTypeOptions()
.and()
.xssProtection()
.and()
//禁用緩存
.cacheControl()
.and()
.httpStrictTransportSecurity()
.and()
//禁用頁(yè)面鑲嵌frame劫持安全協(xié)議 // 防止iframe 造成跨域
.frameOptions().disable();
}
進(jìn)行登錄測(cè)試,驗(yàn)證結(jié)果:
圖片
注:響應(yīng)中有token
查看redis。成功保存進(jìn)了redis
圖片
接口權(quán)限校驗(yàn)
Spring Security使用FilterSecurityInterceptor過濾器來進(jìn)行URL權(quán)限校驗(yàn),實(shí)際使用流程大致如下:
正常情況的接口權(quán)限判斷:
返回那些可以訪問當(dāng)前url的角色
1、定義一個(gè)MyFilterInvocationSecurityMetadataSource實(shí)現(xiàn)FilterInvocationSecurityMetadataSource類,重寫getAttributes方法。
方法的作用是:返回哪些角色可以訪問當(dāng)前url,這個(gè)肯定是從數(shù)據(jù)庫(kù)中獲取。要注意的是對(duì)于PathVariable傳參的url,數(shù)據(jù)庫(kù)中存的是這樣的:/getUserByName/{name}。但實(shí)際訪問的url中name是具體的值。類似的/user/getUserById 也可以匹配 /user/getUserById?1。
package com.aliyu.security.provider;
import com.aliyu.service.role.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
*@create:
*@description: 第一步:數(shù)據(jù)庫(kù)查詢所有權(quán)限出來:
* 之所以要所有權(quán)限,因?yàn)閿?shù)據(jù)庫(kù)url和實(shí)際請(qǐng)求url并不能直接匹配需要。比方:/user/getUserById 匹配 /user/getUserById?1
* 第二步:通過httpUrl匹配器找出允許訪問當(dāng)前請(qǐng)求的角色列表(哪些角色可以訪問此請(qǐng)求)
*/
@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private RoleService roleService;
/**
* 返回當(dāng)前URL允許訪問的角色列表
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//入?yún)⑥D(zhuǎn)為HttpServletRequest
FilterInvocation fi = (FilterInvocation) object;
HttpServletRequest request = fi.getRequest();
//從數(shù)據(jù)庫(kù)中查詢系統(tǒng)所有的權(quán)限,格式為<"權(quán)限url","能訪問url的逗號(hào)分隔的roleid">
List<Map<String, String>> allUrlRoleMap = roleService.getAllUrlRoleMap();
for (Map<String, String> urlRoleMap : allUrlRoleMap) {
String url = urlRoleMap.get("url");
String roles = urlRoleMap.get("roles");
//new AntPathRequestMatcher創(chuàng)建httpUrl匹配器:里面url匹配規(guī)則已經(jīng)給我們弄好了,
// 能夠支持校驗(yàn)PathVariable傳參的url(例如:/getUserByName/{name})
// 也能支持 /user/getUserById 匹配 /user/getUserById?1
AntPathRequestMatcher matcher = new AntPathRequestMatcher(url);
if (matcher.matches(request)){ //當(dāng)前請(qǐng)求與httpUrl匹配器進(jìn)行匹配
return SecurityConfig.createList(roles.split(","));
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
注:
1. 方案一是初始化的時(shí)候加載所有權(quán)限,一次就好了。
2. 方案二每次請(qǐng)求都會(huì)去重新加載系統(tǒng)所有權(quán)限,好處就是不用擔(dān)心權(quán)限修改的問題。(本次實(shí)現(xiàn)方案)
3. 方案三利用Redis緩存
判斷當(dāng)前用戶是否擁有訪問當(dāng)前url的角色
定義一個(gè)MyAccessDecisionManager:通過實(shí)現(xiàn)AccessDecisionManager接口自定義一個(gè)決策管理器,判斷是否有訪問權(quán)限。上一步MyFilterInvocationSecurityMetadataSource中返回的當(dāng)前請(qǐng)求可以訪問角色列表會(huì)傳到這里的decide方法里面(如果沒有角色的話,不會(huì)進(jìn)入decide方法。
正常情況你訪問的url必然和某個(gè)角色關(guān)聯(lián),如果沒有關(guān)聯(lián)就不應(yīng)該可以訪問)。decide方法傳了當(dāng)前登錄用戶擁有的角色,通過判斷用戶擁有的角色中是否有一個(gè)角色和當(dāng)前url可以訪問的角色匹配。如果匹配,權(quán)限校驗(yàn)通過。
package com.aliyu.security.provider;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Iterator;
/**
*@create:
*@description: 接口權(quán)限判斷(根據(jù)MyFilterInvocationSecurityMetadataSource獲取到的請(qǐng)求需要的角色
* 和當(dāng)前登錄人的角色進(jìn)行比較)
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
//循環(huán)請(qǐng)求需要的角色,只要當(dāng)前用戶擁有的角色中包含請(qǐng)求需要的角色中的一個(gè),就算通過。
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while(iterator.hasNext()){
ConfigAttribute configAttribute = iterator.next();
String needCode = configAttribute.getAttribute();
//獲取到了登錄用戶的所有角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (StringUtils.equals(authority.getAuthority(), needCode)) {
return;
}
}
}
throw new AccessDeniedException("當(dāng)前訪問沒有權(quán)限");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return false;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
處理匿名用戶訪問無權(quán)限資源
1、定義一個(gè)CustomAuthenticationEntryPoint實(shí)現(xiàn)AuthenticationEntryPoint處理匿名用戶訪問無權(quán)限資源(可以理解為未登錄的用戶訪問,確實(shí)有些接口是可以不登錄也能訪問的,比較少,我們?cè)赪ebSecurityConfig已經(jīng)配置過了。如果多的話,需要另外考慮從數(shù)據(jù)庫(kù)中獲取,并且權(quán)限需要加一個(gè)標(biāo)志它為匿名用戶可訪問)。
package com.aliyu.security.handler;
import com.aliyu.common.util.JackJsonUtil;
import com.aliyu.entity.common.vo.ResponseFactory;
import com.aliyu.security.constant.MessageConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static com.aliyu.entity.common.exception.CodeMsgEnum.MOVED_PERMANENTLY;
/**
* 未登錄重定向處理器
* <p>
* 未登錄狀態(tài)下訪問需要登錄的接口
*
* @author
*/
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
//原來不需要登錄的接口,現(xiàn)在需要登錄了,所以叫永久移動(dòng)
String message = JackJsonUtil.object2String(
ResponseFactory.fail(MOVED_PERMANENTLY, MessageConstant.NOT_LOGGED_IN)
);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("未登錄重定向!");
}
response.getWriter().write(message);
}
}
處理登陸認(rèn)證過的用戶訪問無權(quán)限資源
2、定義一個(gè)CustomAccessDeniedHandler 實(shí)現(xiàn)AccessDeniedHandler處理登陸認(rèn)證過的用戶訪問無權(quán)限資源。
package com.aliyu.security.handler;
import com.aliyu.common.util.JackJsonUtil;
import com.aliyu.entity.common.exception.CodeMsgEnum;
import com.aliyu.entity.common.vo.ResponseFactory;
import com.aliyu.security.constant.MessageConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 拒絕訪問處理器(登錄狀態(tài)下,訪問沒有權(quán)限的方法時(shí)會(huì)進(jìn)入此處理器)
*
* @author
*/
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
String message = JackJsonUtil.object2String(
ResponseFactory.fail(CodeMsgEnum.UNAUTHORIZED, MessageConstant.NO_ACCESS)
);
if(LOGGER.isDebugEnabled()){
LOGGER.debug("沒有權(quán)限訪問!");
}
response.getWriter().write(message);
}
}
配置到WebSecurityConfig
package com.aliyu.security.config;
import com.aliyu.filter.LoginFilter;
import com.aliyu.security.handler.*;
import com.aliyu.security.provider.MyAccessDecisionManager;
import com.aliyu.security.provider.MyFilterInvocationSecurityMetadataSource;
import com.aliyu.security.provider.UserVerifyAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
import org.springframework.session.web.http.HttpSessionIdResolver;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserVerifyAuthenticationProvider authenticationManager;//認(rèn)證用戶類
@Autowired
private CustomAuthenticationSuccessHandler successHandler;//登錄認(rèn)證成功處理類
@Autowired
private CustomAuthenticationFailureHandler failureHandler;//登錄認(rèn)證失敗處理類
@Autowired
private MyFilterInvocationSecurityMetadataSource securityMetadataSource;//返回當(dāng)前URL允許訪問的角色列表
@Autowired
private MyAccessDecisionManager accessDecisionManager;//除登錄登出外所有接口的權(quán)限校驗(yàn)
/**
* 密碼加密
* @return
*/
@Bean
@ConditionalOnMissingBean(PasswordEncoder.class)
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置 HttpSessionIdResolver Bean
* 登錄之后將會(huì)在 Response Header x-auth-token 中 返回當(dāng)前 sessionToken
* 將token存儲(chǔ)在前端 每次調(diào)用的時(shí)候 Request Header x-auth-token 帶上 sessionToken
*/
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
/**
* Swagger等靜態(tài)資源不進(jìn)行攔截
*/
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/error",
"/webjars/**",
"/resources/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置一些不需要登錄就可以訪問的接口
.antMatchers("/demo/**", "/about/**").permitAll()
//任何尚未匹配的URL只需要用戶進(jìn)行身份驗(yàn)證
.anyRequest().authenticated()
//登錄后的接口權(quán)限校驗(yàn)
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(accessDecisionManager);
object.setSecurityMetadataSource(securityMetadataSource);
return object;
}
})
.and()
//配置登出處理
.logout().logoutUrl("/logout")
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.clearAuthentication(true)
.and()
//用來解決匿名用戶訪問無權(quán)限資源時(shí)的異常
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
//用來解決登陸認(rèn)證過的用戶訪問無權(quán)限資源時(shí)的異常
.accessDeniedHandler(new CustomAccessDeniedHandler())
.and()
//配置登錄過濾器
.addFilter(new LoginFilter(authenticationManager, successHandler, failureHandler))
.csrf().disable();
//配置頭部
http.headers()
.contentTypeOptions()
.and()
.xssProtection()
.and()
//禁用緩存
.cacheControl()
.and()
.httpStrictTransportSecurity()
.and()
//禁用頁(yè)面鑲嵌frame劫持安全協(xié)議 // 防止iframe 造成跨域
.frameOptions().disable();
}
}
3、其他
特別的,我們認(rèn)為如果一個(gè)接口屬于當(dāng)前系統(tǒng),那么它就應(yīng)該有對(duì)應(yīng)可以訪問的角色。這樣的接口才會(huì)被我們限制住。如果一個(gè)接口只是在當(dāng)前系統(tǒng)定義了,而沒有指明它的角色,這樣的接口是不會(huì)被我們限制的。
注意點(diǎn)
下面的代碼,本意是想配置一些不需要登錄也可以訪問的接口。
圖片
但是測(cè)試的時(shí)候發(fā)現(xiàn),任何接口的調(diào)用都會(huì)進(jìn)入這里MyFilterInvocationSecurityMetadataSource getAttriButes方法,包括我webSecurityConfig里配置的不需要登錄的url。結(jié)果就是不需要登錄的url和沒有配置角色的接口權(quán)限一樣待遇,要么都能訪問,要么都不能訪問?。。?/p>
所以如上圖,我在這里配置了不需要登錄的接口(因?yàn)椴恢廊绾螐膚ebSercurityConfig中獲取,干脆就配置在這里了),去掉了webSercurityConfig中的相應(yīng)配置。