![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SY5UDNiJWZ5MWNygDOwIzYyMjN0gDMhljM0kTMkFzYm9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
一、前言
本篇文章将講述
Spring Security 動态配置設定url權限,未登入權限控制,登入過後根據登入使用者角色授予通路url權限
基本環境
- spring-boot 2.1.8
- mybatis-plus 2.2.0
- mysql 資料庫
- maven項目
Spring Security入門學習可參考之前文章:
- SpringBoot內建Spring Security入門體驗(一)
- Spring Security 自定義登入認證(二)
二、資料庫建表
表關系簡介:
- 使用者表
關聯 角色表t_sys_user
兩者建立中間關系表t_sys_role
t_sys_user_role
- 角色表
關聯 權限表t_sys_role
兩者建立中間關系表t_sys_permission
t_sys_role_permission
- 最終展現效果為目前登入使用者所具備的角色關聯能通路的所有url,隻要給角色配置設定相應的url權限即可
溫馨小提示 :這裡邏輯根據個人業務來定義,小編這裡講解案例隻給使用者對應的角色配置設定通路權限,像其它的 直接給使用者配置設定權限等等可以自己實作
表模拟資料如下:
三、Spring Security 動态權限控制
1、未登入通路權限控制
自定義
AdminAuthenticationEntryPoint
類實作
AuthenticationEntryPoint
類
這裡是認證權限入口 -> 即在未登入的情況下通路所有接口都會攔截到此(除了放行忽略接口)
溫馨小提示 :和
ResponseUtils
是小編這裡模拟前後端分離情況下傳回json格式資料所使用工具類,具體實作可參考文末給出的demo源碼
ApiResult
@Component
public class AdminAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
ResponseUtils.out(response, ApiResult.fail("未登入!!!"));
}
}
2、自定義過濾器 MyAuthenticationFilter
繼承 OncePerRequestFilter
實作通路鑒權
MyAuthenticationFilter
OncePerRequestFilter
每次通路接口都會經過此,我們可以在這裡記錄請求參數、響應内容,或者處理前後端分離情況下,以token換使用者權限資訊,token是否過期,請求頭類型是否正确,防止非法請求等等
-
方法:記錄請求消息體logRequestBody()
-
方法:記錄響應消息體logResponseBody()
【注:請求的
HttpServletRequest流隻能讀一次
,下一次就不能讀取了,是以這裡要使用自定義的
MultiReadHttpServletRequest
工具解決流隻能讀一次的問題,響應同理,具體可參考文末demo源碼實作】
@Slf4j
@Component
public class MyAuthenticationFilter extends OncePerRequestFilter {
private final UserDetailsServiceImpl userDetailsService;
protected MyAuthenticationFilter(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("請求頭類型: " + request.getContentType());
if ((request.getContentType() == null && request.getContentLength() > 0) || (request.getContentType() != null && !request.getContentType().contains(Constants.REQUEST_HEADERS_CONTENT_TYPE))) {
filterChain.doFilter(request, response);
return;
}
MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request);
MultiReadHttpServletResponse wrappedResponse = new MultiReadHttpServletResponse(response);
StopWatch stopWatch = new StopWatch();
try {
stopWatch.start();
// 記錄請求的消息體
logRequestBody(wrappedRequest);
// String token = "123";
// 前後端分離情況下,前端登入後将token儲存在cookie中,每次通路接口時通過token去拿使用者權限
String token = wrappedRequest.getHeader(Constants.REQUEST_HEADER);
log.debug("背景檢查令牌:{}", token);
if (StringUtils.isNotBlank(token)) {
// 檢查token
SecurityUser securityUser = userDetailsService.getUserByToken(token);
if (securityUser == null || securityUser.getCurrentUserInfo() == null) {
throw new AccessDeniedException("TOKEN已過期,請重新登入!");
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
// 全局注入角色權限資訊和登入使用者基本資訊
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
stopWatch.stop();
long usedTimes = stopWatch.getTotalTimeMillis();
// 記錄響應的消息體
logResponseBody(wrappedRequest, wrappedResponse, usedTimes);
}
}
private String logRequestBody(MultiReadHttpServletRequest request) {
MultiReadHttpServletRequest wrapper = request;
if (wrapper != null) {
try {
String bodyJson = wrapper.getBodyJsonStrByJson(request);
String url = wrapper.getRequestURI().replace("//", "/");
System.out.println("-------------------------------- 請求url: " + url + " --------------------------------");
Constants.URL_MAPPING_MAP.put(url, url);
log.info("`{}` 接收到的參數: {}",url , bodyJson);
return bodyJson;
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
private void logResponseBody(MultiReadHttpServletRequest request, MultiReadHttpServletResponse response, long useTime) {
MultiReadHttpServletResponse wrapper = response;
if (wrapper != null) {
byte[] buf = wrapper.getBody();
if (buf.length > 0) {
String payload;
try {
payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
} catch (UnsupportedEncodingException ex) {
payload = "[unknown]";
}
log.info("`{}` 耗時:{}ms 傳回的參數: {}", Constants.URL_MAPPING_MAP.get(request.getRequestURI()), useTime, payload);
}
}
}
}
3、自定義 UserDetailsServiceImpl
實作 UserDetailsService
和 自定義 SecurityUser
實作 UserDetails
認證使用者詳情
UserDetailsServiceImpl
UserDetailsService
SecurityUser
UserDetails
這個在上一篇文章中也提及過,但上次未做角色權限處理,這次我們來一起加上吧
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private UserRoleMapper userRoleMapper;
/***
* 根據賬号擷取使用者資訊
* @param username:
* @return: org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從資料庫中取出使用者資訊
List<User> userList = userMapper.selectList(new EntityWrapper<User>().eq("username", username));
User user;
// 判斷使用者是否存在
if (!CollectionUtils.isEmpty(userList)) {
user = userList.get(0);
} else {
throw new UsernameNotFoundException("使用者名不存在!");
}
// 傳回UserDetails實作類
return new SecurityUser(user, getUserRoles(user.getId()));
}
/***
* 根據token擷取使用者權限與基本資訊
*
* @param token:
* @return: com.zhengqing.config.security.dto.SecurityUser
*/
public SecurityUser getUserByToken(String token) {
User user = null;
List<User> loginList = userMapper.selectList(new EntityWrapper<User>().eq("token", token));
if (!CollectionUtils.isEmpty(loginList)) {
user = loginList.get(0);
}
return user != null ? new SecurityUser(user, getUserRoles(user.getId())) : null;
}
/**
* 根據使用者id擷取角色權限資訊
*
* @param userId
* @return
*/
private List<Role> getUserRoles(Integer userId) {
List<UserRole> userRoles = userRoleMapper.selectList(new EntityWrapper<UserRole>().eq("user_id", userId));
List<Role> roleList = new LinkedList<>();
for (UserRole userRole : userRoles) {
Role role = roleMapper.selectById(userRole.getRoleId());
roleList.add(role);
}
return roleList;
}
}
這裡再說下自定義
SecurityUser
是因為Spring Security自帶的
UserDetails
(存儲目前使用者基本資訊) 有時候可能不滿足我們的需求,是以我們可以自己定義一個來擴充我們的需求
getAuthorities()
方法:即授予目前使用者角色權限資訊
@Data
@Slf4j
public class SecurityUser implements UserDetails {
/**
* 目前登入使用者
*/
private transient User currentUserInfo;
/**
* 角色
*/
private transient List<Role> roleList;
public SecurityUser() { }
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
public SecurityUser(User user, List<Role> roleList) {
if (user != null) {
this.currentUserInfo = user;
this.roleList = roleList;
}
}
/**
* 擷取目前使用者所具有的角色
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (!CollectionUtils.isEmpty(this.roleList)) {
for (Role role : this.roleList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getCode());
authorities.add(authority);
}
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
4、自定義 UrlFilterInvocationSecurityMetadataSource
實作 FilterInvocationSecurityMetadataSource
重寫 getAttributes()
方法 擷取通路該url所需要的角色權限資訊
UrlFilterInvocationSecurityMetadataSource
FilterInvocationSecurityMetadataSource
getAttributes()
執行完之後到 下一步
UrlAccessDecisionManager
中認證權限
@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
PermissionMapper permissionMapper;
@Autowired
RolePermissionMapper rolePermissionMapper;
@Autowired
RoleMapper roleMapper;
/***
* 傳回該url所需要的使用者權限資訊
*
* @param object: 儲存請求url資訊
* @return: null:辨別不需要任何權限都可以通路
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 擷取目前請求url
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// TODO 忽略url請放在此處進行過濾放行
if ("/login".equals(requestUrl) || requestUrl.contains("logout")) {
return null;
}
// 資料庫中所有url
List<Permission> permissionList = permissionMapper.selectList(null);
for (Permission permission : permissionList) {
// 擷取該url所對應的權限
if (requestUrl.equals(permission.getUrl())) {
List<RoleMenu> permissions = rolePermissionMapper.selectList(new EntityWrapper<RoleMenu>().eq("permission_id", permission.getId()));
List<String> roles = new LinkedList<>();
if (!CollectionUtils.isEmpty(permissions)){
Integer roleId = permissions.get(0).getRoleId();
Role role = roleMapper.selectById(roleId);
roles.add(role.getCode());
}
// 儲存該url對應角色權限資訊
return SecurityConfig.createList(roles.toArray(new String[roles.size()]));
}
}
// 如果資料中沒有找到相應url資源則為非法通路,要求使用者登入再進行操作
return SecurityConfig.createList(Constants.ROLE_LOGIN);
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
5、自定義 UrlAccessDecisionManager
實作 AccessDecisionManager
重寫 decide()
方法 對通路url進行權限認證處理
UrlAccessDecisionManager
AccessDecisionManager
decide()
此處小編的處理邏輯是隻要包含其中一個角色即可通路
@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
/**
* @param authentication: 目前登入使用者的角色資訊
* @param object: 請求url資訊
* @param collection: `UrlFilterInvocationSecurityMetadataSource`中的getAttributes方法傳來的,表示目前請求需要的角色(可能有多個)
* @return: void
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, AuthenticationException {
// 周遊角色
for (ConfigAttribute ca : collection) {
// ① 目前url請求需要的權限
String needRole = ca.getAttribute();
if (Constants.ROLE_LOGIN.equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("未登入!");
} else {
throw new AccessDeniedException("未授權該url!");
}
}
// ② 目前使用者所具有的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
// 隻要包含其中一個角色即可通路
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("請聯系管理者配置設定權限!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
6、自定義無權限處理器 UrlAccessDeniedHandler
實作 AccessDeniedHandler
重寫 handle()
方法
UrlAccessDeniedHandler
AccessDeniedHandler
handle()
在這裡自定義403無權限響應内容,登入過後的權限處理 【
注:要和未登入時的權限處理區分開哦~ 】
@Component
public class UrlAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
ResponseUtils.out(response, ApiResult.fail(403, e.getMessage()));
}
}
7、最後在 Security 核心配置類
中配置以上處理
Security 核心配置類
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 通路鑒權 - 認證token、簽名...
*/
private final MyAuthenticationFilter myAuthenticationFilter;
/**
* 通路權限認證異常處理
*/
private final AdminAuthenticationEntryPoint adminAuthenticationEntryPoint;
/**
* 使用者密碼校驗過濾器
*/
private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter;
// 上面是登入認證相關 下面為url權限相關 - ========================================================================================
/**
* 擷取通路url所需要的角色資訊
*/
private final UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
/**
* 認證權限處理 - 将上面所獲得角色權限與目前登入使用者的角色做對比,如果包含其中一個角色即可正常通路
*/
private final UrlAccessDecisionManager urlAccessDecisionManager;
/**
* 自定義通路無權限接口時403響應内容
*/
private final UrlAccessDeniedHandler urlAccessDeniedHandler;
public SecurityConfig(MyAuthenticationFilter myAuthenticationFilter, AdminAuthenticationEntryPoint adminAuthenticationEntryPoint, AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter, UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource, UrlAccessDeniedHandler urlAccessDeniedHandler, UrlAccessDecisionManager urlAccessDecisionManager) {
this.myAuthenticationFilter = myAuthenticationFilter;
this.adminAuthenticationEntryPoint = adminAuthenticationEntryPoint;
this.adminAuthenticationProcessingFilter = adminAuthenticationProcessingFilter;
this.urlFilterInvocationSecurityMetadataSource = urlFilterInvocationSecurityMetadataSource;
this.urlAccessDeniedHandler = urlAccessDeniedHandler;
this.urlAccessDecisionManager = urlAccessDecisionManager;
}
/**
* 權限配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();
// 禁用CSRF 開啟跨域
http.csrf().disable().cors();
// 未登入認證異常
http.exceptionHandling().authenticationEntryPoint(adminAuthenticationEntryPoint);
// 登入過後通路無權限的接口時自定義403響應内容
http.exceptionHandling().accessDeniedHandler(urlAccessDeniedHandler);
// url權限認證處理
registry.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
o.setAccessDecisionManager(urlAccessDecisionManager);
return o;
}
});
// 不建立會話 - 即通過前端傳token到背景過濾器中驗證是否存在通路權限
// http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 辨別通路 `/home` 這個接口,需要具備`ADMIN`角色
// registry.antMatchers("/home").hasRole("ADMIN");
// 辨別隻能在 伺服器本地ip[127.0.0.1或localhost] 通路 `/home` 這個接口,其他ip位址無法通路
registry.antMatchers("/home").hasIpAddress("127.0.0.1");
// 允許匿名的url - 可了解為放行接口 - 多個接口使用,分割
registry.antMatchers("/login", "/index").permitAll();
// registry.antMatchers("/**").access("hasAuthority('admin')");
// OPTIONS(選項):查找适用于一個特定網址資源的通訊選擇。 在不需執行具體的涉及資料傳輸的動作情況下, 允許用戶端來确定與資源相關的選項以及 / 或者要求, 或是一個伺服器的性能
registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
// 自動登入 - cookie儲存方式
registry.and().rememberMe();
// 其餘所有請求都需要認證
registry.anyRequest().authenticated();
// 防止iframe 造成跨域
registry.and().headers().frameOptions().disable();
// 自定義過濾器在登入時認證使用者名、密碼
http.addFilterAt(adminAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(myAuthenticationFilter, BasicAuthenticationFilter.class);
}
/**
* 忽略攔截url或靜态資源檔案夾 - web.ignoring(): 會直接過濾該url - 将不會經過Spring Security過濾器鍊
* http.permitAll(): 不會繞開springsecurity驗證,相當于是允許該路徑通過
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.GET,
"/favicon.ico",
"/*.html",
"/**/*.css",
"/**/*.js");
}
}
四、編寫測試代碼
控制層:
@Slf4j
@RestController
public class IndexController {
@GetMapping("/")
public ModelAndView showHome() {
return new ModelAndView("home.html");
}
@GetMapping("/index")
public String index() {
return "Hello World ~";
}
@GetMapping("/login")
public ModelAndView login() {
return new ModelAndView("login.html");
}
@GetMapping("/home")
public String home() {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
log.info("登陸人:" + name);
return "Hello~ " + name;
}
@GetMapping(value ="/admin")
// 通路路徑`/admin` 具有`ADMIN`角色權限 【這種是寫死方式】
// @PreAuthorize("hasPermission('/admin','ADMIN')")
public String admin() {
return "Hello~ 管理者";
}
@GetMapping("/test")
public String test() {
return "Hello~ 測試權限通路接口";
}
}
頁面和其它相關代碼這裡就不貼出來了,具體可參考文末demo源碼
五、運作通路測試效果
1、未登入時
2、登入過後如果有權限則正常通路
3、登入過後,沒有權限
這裡我們可以修改資料庫角色權限關聯表
t_sys_role_permission
來進行測試哦 ~
Security 動态url權限也就是依賴這張表來判斷的,隻要修改這張表配置設定角色對應url權限資源,使用者通路url時就會動态的去判斷,無需做其他處理,如果是将權限資訊放在了緩存中,修改表資料時及時更新緩存即可!
4、登入過後,通路資料庫中沒有配置的url 并且 在Security中沒有忽略攔截的url時
六、總結
- 自定義未登入權限處理器
- 自定義未登入時通路無權限url響應内容AdminAuthenticationEntryPoint
- 自定義通路鑒權過濾器
- 記錄請求響應日志、是否合法通路,驗證token過期等MyAuthenticationFilter
- 自定義
- 擷取通路該url所需要的角色權限UrlFilterInvocationSecurityMetadataSource
- 自定義
- 對通路url進行權限認證處理UrlAccessDecisionManager
- 自定義
- 登入過後通路無權限url失敗處理器 - 自定義403無權限響應内容UrlAccessDeniedHandler
- 在
中配置以上處理器和過濾器Security核心配置類
Security動态權限相關代碼:
本文案例demo源碼
https://gitee.com/zhengqingya/java-workspace