天天看點

websecurity連接配接mysql_Spring Security 動态url權限控制(三)

一、前言

本篇文章将講述Spring Security 動态配置設定url權限,未登入權限控制,登入過後根據登入使用者角色授予通路url權限

基本環境

spring-boot 2.1.8

mybatis-plus 2.2.0

mysql 資料庫

maven項目

Spring Security入門學習可參考之前文章:

二、資料庫建表

websecurity連接配接mysql_Spring Security 動态url權限控制(三)

表關系簡介:

使用者表t_sys_user 關聯 角色表t_sys_role 兩者建立中間關系表t_sys_user_role

角色表t_sys_role 關聯 權限表t_sys_permission 兩者建立中間關系表t_sys_role_permission

最終展現效果為目前登入使用者所具備的角色關聯能通路的所有url,隻要給角色配置設定相應的url權限即可

溫馨小提示:這裡邏輯根據個人業務來定義,小編這裡講解案例隻給使用者對應的角色配置設定通路權限,像其它的 直接給使用者配置設定權限等等可以自己實作

表模拟資料如下:

websecurity連接配接mysql_Spring Security 動态url權限控制(三)

三、Spring Security 動态權限控制

1、未登入通路權限控制

自定義AdminAuthenticationEntryPoint類實作AuthenticationEntryPoint類

這裡是認證權限入口 -> 即在未登入的情況下通路所有接口都會攔截到此(除了放行忽略接口)

溫馨小提示:ResponseUtils和ApiResult是小編這裡模拟前後端分離情況下傳回json格式資料所使用工具類,具體實作可參考文末給出的demo源碼

@Component

public class AdminAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override

public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {

ResponseUtils.out(response, ApiResult.fail("未登入!!!"));

}

}

2、自定義過濾器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 認證使用者詳情

這個在上一篇文章中也提及過,但上次未做角色權限處理,這次我們來一起加上吧

@Service("userDetailsService")

public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired

private UserMapper userMapper;

@Autowired

private RoleMapper roleMapper;

@Autowired

private UserRoleMapper userRoleMapper;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

// 從資料庫中取出使用者資訊

List userList = userMapper.selectList(new EntityWrapper().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()));

}

public SecurityUser getUserByToken(String token) {

User user = null;

List loginList = userMapper.selectList(new EntityWrapper().eq("token", token));

if (!CollectionUtils.isEmpty(loginList)) {

user = loginList.get(0);

}

return user != null ? new SecurityUser(user, getUserRoles(user.getId())) : null;

}

private List getUserRoles(Integer userId) {

List userRoles = userRoleMapper.selectList(new EntityWrapper().eq("user_id", userId));

List roleList = new LinkedList<>();

for (UserRole userRole : userRoles) {

Role role = roleMapper.selectById(userRole.getRoleId());

roleList.add(role);

}

return roleList;

}

}

這裡再說下自定義SecurityUser是因為Spring Security自帶的 UserDetails (存儲目前使用者基本資訊) 有時候可能不滿足我們的需求,是以我們可以自己定義一個來擴充我們的需求

websecurity連接配接mysql_Spring Security 動态url權限控制(三)

getAuthorities()方法:即授予目前使用者角色權限資訊

@Data

@Slf4j

public class SecurityUser implements UserDetails {

private transient User currentUserInfo;

private transient List roleList;

public SecurityUser() { }

public SecurityUser(User user) {

if (user != null) {

this.currentUserInfo = user;

}

}

public SecurityUser(User user, List roleList) {

if (user != null) {

this.currentUserInfo = user;

this.roleList = roleList;

}

}

@Override

public Collection extends GrantedAuthority> getAuthorities() {

Collection 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所需要的角色權限資訊

執行完之後到 下一步 UrlAccessDecisionManager 中認證權限

@Component

public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

@Autowired

PermissionMapper permissionMapper;

@Autowired

RolePermissionMapper rolePermissionMapper;

@Autowired

RoleMapper roleMapper;

@Override

public Collection getAttributes(Object object) throws IllegalArgumentException {

// 擷取目前請求url

String requestUrl = ((FilterInvocation) object).getRequestUrl();

// TODO 忽略url請放在此處進行過濾放行

if ("/login".equals(requestUrl) || requestUrl.contains("logout")) {

return null;

}

// 資料庫中所有url

List permissionList = permissionMapper.selectList(null);

for (Permission permission : permissionList) {

// 擷取該url所對應的權限

if (requestUrl.equals(permission.getUrl())) {

List permissions = rolePermissionMapper.selectList(new EntityWrapper().eq("permission_id", permission.getId()));

List 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 getAllConfigAttributes() {

return null;

}

@Override

public boolean supports(Class> aClass) {

return FilterInvocation.class.isAssignableFrom(aClass);

}

}

5、自定義UrlAccessDecisionManager實作AccessDecisionManager重寫decide()方法 對通路url進行權限認證處理

此處小編的處理邏輯是隻要包含其中一個角色即可通路

@Component

public class UrlAccessDecisionManager implements AccessDecisionManager {

@Override

public void decide(Authentication authentication, Object object, Collection 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()方法

在這裡自定義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 核心配置類中配置以上處理

@Configuration

@EnableWebSecurity

@EnableGlobalMethodSecurity(prePostEnabled = true)

public class SecurityConfig extends WebSecurityConfigurerAdapter {

private final MyAuthenticationFilter myAuthenticationFilter;

private final AdminAuthenticationEntryPoint adminAuthenticationEntryPoint;

private final AdminAuthenticationProcessingFilter adminAuthenticationProcessingFilter;

// 上面是登入認證相關 下面為url權限相關 - ========================================================================================

private final UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;

private final UrlAccessDecisionManager urlAccessDecisionManager;

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;

}

@Override

protected void configure(HttpSecurity http) throws Exception {

ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = http.antMatcher("

@Override

public void configure(WebSecurity web) throws Exception {

web.ignoring().antMatchers(HttpMethod.GET,

"/favicon.ico",

"*.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、未登入時

websecurity連接配接mysql_Spring Security 動态url權限控制(三)

2、登入過後如果有權限則正常通路

websecurity連接配接mysql_Spring Security 動态url權限控制(三)

3、登入過後,沒有權限

這裡我們可以修改資料庫角色權限關聯表t_sys_role_permission來進行測試哦 ~

Security 動态url權限也就是依賴這張表來判斷的,隻要修改這張表配置設定角色對應url權限資源,使用者通路url時就會動态的去判斷,無需做其他處理,如果是将權限資訊放在了緩存中,修改表資料時及時更新緩存即可!

websecurity連接配接mysql_Spring Security 動态url權限控制(三)
websecurity連接配接mysql_Spring Security 動态url權限控制(三)

4、登入過後,通路資料庫中沒有配置的url 并且 在Security中沒有忽略攔截的url時

websecurity連接配接mysql_Spring Security 動态url權限控制(三)

六、總結

自定義未登入權限處理器AdminAuthenticationEntryPoint - 自定義未登入時通路無權限url響應内容

自定義通路鑒權過濾器MyAuthenticationFilter - 記錄請求響應日志、是否合法通路,驗證token過期等

自定義UrlFilterInvocationSecurityMetadataSource - 擷取通路該url所需要的角色權限

自定義UrlAccessDecisionManager - 對通路url進行權限認證處理

自定義UrlAccessDeniedHandler - 登入過後通路無權限url失敗處理器 - 自定義403無權限響應内容

在Security核心配置類中配置以上處理器和過濾器

Security動态權限相關代碼:

websecurity連接配接mysql_Spring Security 動态url權限控制(三)

本文案例demo源碼