Spring Security實(shí)現(xiàn)動態(tài)路由權(quán)限控制方式
Spring Security實(shí)現(xiàn)動態(tài)路由權(quán)限控制
主要步驟如下:
- 1、SecurityUser implements UserDetails 接口中的方法
- 2、自定義認(rèn)證:UserDetailsServiceImpl implements UserDetailsService
- 3、添加登錄過濾器LoginFilter extends OncePerRequestFilter
每次訪問接口都會經(jīng)過此,我們可以在這里記錄請求參數(shù)、響應(yīng)內(nèi)容,或者處理前后端分離情況下, 以token換用戶權(quán)限信息,token是否過期,請求頭類型是否正確,防止非法請求等等
- 4、動態(tài)權(quán)限過濾器,用于實(shí)現(xiàn)基于路徑的動態(tài)權(quán)限過濾:SecurityFilter extends AbstractSecurityInterceptor implements Filter
- 5、未登錄訪問控制類:AdminAuthenticationEntryPoint implements AuthenticationEntryPoint
- 6、獲取訪問URL所需要的角色信息類:UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource
- 7、權(quán)限認(rèn)證處理類:UrlAccessDecisionManager implements AccessDecisionManager,認(rèn)證失敗拋出:AccessDeniedException 異常
- 8、權(quán)限認(rèn)證失敗后的處理類:UrlAccessDeniedHandler implements AccessDeniedHandler
- 9、核心配置SecurityConfig

代碼實(shí)現(xiàn)
1、SecurityUser implements UserDetails 接口中的方法
package com.example.security.url.entity;
import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author Deyou Kong
* @description security驗(yàn)證用戶
* @date 2023/2/9 3:01 下午
*/
@Data
@Slf4j
@ToString
public class SecurityUser implements UserDetails {
/**
* 用戶信息
*/
private User user;
/**
* 用戶擁有的角色列表
*/
private List<Role> roles;
public SecurityUser() { }
public SecurityUser(User user) {
if (user != null) {
this.user = user;
}
}
public SecurityUser(User user, List<Role> roleList) {
if (user != null) {
this.user = user;
this.roles = roleList;
}
}
/**
* 獲取當(dāng)前用戶所具有的角色
* @return 返回角色列表 List<Role.getCode()>
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (!CollectionUtils.isEmpty(this.roles)) {
for (Role role : this.roles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getCode());
authorities.add(authority);
}
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.getStatus() == 1 ? true: false;
}
}
2、自定義認(rèn)證:UserDetailsServiceImpl implements UserDetailsService
package com.example.security.url.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.security.url.constants.ResultConstant;
import com.example.security.url.dao.RoleMapper;
import com.example.security.url.dao.UserMapper;
import com.example.security.url.dao.UserRoleMapper;
import com.example.security.url.entity.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
UserMapper userMapper;
@Resource
UserRoleMapper userRoleMapper;
@Resource
RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("UserDetailsService實(shí)現(xiàn)類");
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(queryWrapper);
//如果用戶被禁用,則不再查詢權(quán)限表
if (user == null){
// 拋出異常,會被LoginFailHandlerEntryPoint捕獲
throw new UsernameNotFoundException(ResultConstant.USER_NOT_EXIST);
//return null;
}
return new SecurityUser(user, getUserRoles(user.getId()));
}
/**
* 根據(jù)用戶id獲取角色權(quán)限信息
*
* @param userId
* @return
*/
private List<Role> getUserRoles(Integer userId) {
LambdaQueryWrapper<UserRole> userRoleLambdaQueryWrapper = new LambdaQueryWrapper<>();
userRoleLambdaQueryWrapper.eq(UserRole::getUserId, userId);
List<UserRole> userRoles = userRoleMapper.selectList(userRoleLambdaQueryWrapper);
// 判斷用戶有沒有角色,沒有角色,直接返回空列表
if (CollectionUtils.isEmpty(userRoles)){
return new ArrayList<>();
}
Set<Integer> roleIdSet = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
List<Role> roles = roleMapper.selectBatchIds(roleIdSet);
if (CollectionUtils.isEmpty(roles)){
return new ArrayList<>();
}
return roles;
}
}3、添加登錄過濾器LoginFilter extends OncePerRequestFilter
每次訪問接口都會經(jīng)過此,我們可以在這里記錄請求參數(shù)、響應(yīng)內(nèi)容等日志,或者處理前后端分離情況下,以token換用戶權(quán)限信息,token是否過期,請求頭類型是否正確,防止非法請求等等
package com.example.security.url.filter;
import com.example.security.url.common.result.CommonResult;
import com.example.security.url.exception.LoginException;
import com.example.security.url.property.IgnoreUrlsConfig;
import com.example.security.url.constants.ResultConstant;
import com.example.security.url.utils.JwtTokenUtil;
import com.example.security.url.utils.ResponseUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 請求的HttpServletRequest流只能讀一次,下一次就不能讀取了,
* 因此這里要使用自定義的MultiReadHttpServletRequest工具解決流只能讀一次的問題
*
* @author Deyou Kong
* @description 用戶登錄鑒權(quán)過濾器 filter
* @date 2023/2/10 2:25 下午
*/
@Slf4j
public class LoginFilter extends OncePerRequestFilter {
@Resource
private UserDetailsService userDetailsService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Resource
IgnoreUrlsConfig ignoreUrlsConfig;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenType}")
private String tokenType;
@Value("${server.servlet.context-path}")
private String contextPath;
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
log.info("LoginFilter -> doFilterInternal,請求URL:{}", requestURI);
// 如果requestURI在白名單中直接放行
try {
PathMatcher pathMatcher = new AntPathMatcher();
for (String url : ignoreUrlsConfig.getUrls()) {
String requestUrl = contextPath + url;
if (pathMatcher.match((requestUrl), requestURI)) {
chain.doFilter(request, response);
return;
}
}
// 驗(yàn)證token
String token = request.getHeader(tokenHeader);
if (StringUtils.isAllBlank(token)){
throw new LoginException(ResultConstant.NOT_TOKEN);
}
if (!token.startsWith(tokenType)){
throw new LoginException(ResultConstant.TOKEN_REG_FAIL);
}
String authToken = token.substring(tokenType.length());
if (jwtTokenUtil.isTokenExpired(authToken)){
throw new LoginException(ResultConstant.TOKEN_INVALID);
}
String username = jwtTokenUtil.getUserNameFromToken(authToken);
//if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (username != null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
// token 中的用戶在數(shù)據(jù)庫中查詢到數(shù)據(jù),開始進(jìn)行密碼驗(yàn)證
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
return;
}
}
} catch (LoginException e) {
CommonResult<String> result = CommonResult.loginFailed(e.getMessage());
ResponseUtils.out(response, result);
}catch (Exception e){
e.printStackTrace();
CommonResult<String> result = CommonResult.loginFailed(ResultConstant.SYS_ERROR);
ResponseUtils.out(response, result);
return ;
}
}
}
4、動態(tài)權(quán)限過濾器,用于實(shí)現(xiàn)基于路徑的動態(tài)權(quán)限過濾:SecurityFilter extends AbstractSecurityInterceptor implements Filter
package com.example.security.url.filter;
import com.example.security.url.url.UrlAccessDecisionManager;
import com.example.security.url.property.IgnoreUrlsConfig;
import com.example.security.url.url.UrlFilterInvocationSecurityMetadataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 動態(tài)權(quán)限過濾器,用于實(shí)現(xiàn)基于路徑的動態(tài)權(quán)限過濾
*/
@Slf4j
public class SecurityFilter extends AbstractSecurityInterceptor implements Filter {
@Resource
private UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
@Resource
private IgnoreUrlsConfig ignoreUrlsConfig;
@Value("${server.servlet.context-path}")
private String contextPath;
@Resource
public void setAccessDecisionManager(UrlAccessDecisionManager urlAccessDecisionManager) {
super.setAccessDecisionManager(urlAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
log.info("SecurityFilter動態(tài)權(quán)限過濾器,用于實(shí)現(xiàn)基于路徑的動態(tài)權(quán)限過濾");
/**
* 仿照OncePerRequestFilter,解決Filter執(zhí)行兩次的問題
* 執(zhí)行兩次原因:SecurityConfig中,@Bean和addFilter相當(dāng)于向容器注入了兩次
* 解決辦法:1是去掉@Bean,但Filter中若有引用注入容器的其它資源,則會報(bào)錯
* 2就是request中保存一個Attribute來判斷該請求是否已執(zhí)行過
*/
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (hasAlreadyFilteredAttribute) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
//OPTIONS請求直接放行
if (request.getMethod().equals(HttpMethod.OPTIONS.toString())) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
//白名單請求直接放行
PathMatcher pathMatcher = new AntPathMatcher();
for (String path : ignoreUrlsConfig.getUrls()) {
if (pathMatcher.match(contextPath + path, request.getRequestURI())) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
}
//此處會調(diào)用AccessDecisionManager中的decide方法進(jìn)行鑒權(quán)操作
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
urlFilterInvocationSecurityMetadataSource.clearDataSource();
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public UrlFilterInvocationSecurityMetadataSource obtainSecurityMetadataSource() {
log.info("SecurityFilter返回UrlFilterInvocationSecurityMetadataSource對象");
return urlFilterInvocationSecurityMetadataSource;
}
protected String getAlreadyFilteredAttributeName() {
return this.getClass().getName() + ".FILTERED";
}
}
5、未登錄訪問控制類:AdminAuthenticationEntryPoint implements AuthenticationEntryPoint
package com.example.security.url.filter;
import com.alibaba.fastjson.JSON;
import com.example.security.url.common.result.CommonResult;
import com.example.security.url.utils.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 在實(shí)現(xiàn) UserDetailsService 接口的類中拋出 org.springframework.security.core.userdetails.UsernameNotFoundException 異常都會被此類捕獲
* @author Deyou Kong
* @description 登錄失敗處理類/未登錄,
* @date 2023/2/10 2:19 下午
*/
@Slf4j
public class LoginFailHandlerEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.warn("LoginFailHandlerEntryPoint 登錄失敗處理類");
ResponseUtils.out(response, CommonResult.loginFailed(authException.getLocalizedMessage()));
}
}ResponseUtils 工具類文末附上
6、獲取訪問URL所需要的角色信息類:UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource
package com.example.security.url.url;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.security.url.property.IgnoreUrlsConfig;
import com.example.security.url.constants.ResultConstant;
import com.example.security.url.dao.PermissionMapper;
import com.example.security.url.dao.RoleMapper;
import com.example.security.url.dao.RolePermissionMapper;
import com.example.security.url.entity.Permission;
import com.example.security.url.entity.Role;
import com.example.security.url.entity.RolePermission;
import lombok.extern.slf4j.Slf4j;
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.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Deyou Kong
* @description 訪問URL需要的角色權(quán)限
* @date 2023/2/10 4:19 下午
*/
@Slf4j
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
/**
* 正則匹配匹配
*/
AntPathMatcher pathMatcher = new AntPathMatcher();
@Resource
PermissionMapper permissionMapper;
@Resource
RolePermissionMapper rolePermissionMapper;
@Resource
RoleMapper roleMapper;
@Resource
IgnoreUrlsConfig ignoreUrlsConfig;
private List<ConfigAttribute> allConfigAttributes;
public void clearDataSource() {
allConfigAttributes.clear();
allConfigAttributes = null;
}
/***
* 返回該url所需要的用戶權(quán)限信息
*
* @param object: 儲存請求url信息
* @return: null:標(biāo)識不需要任何權(quán)限都可以訪問
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
log.info("UrlFilterInvocationSecurityMetadataSource獲取請求URL所需角色");
// 獲取當(dāng)前請求url
String requestUrl = ((FilterInvocation) object).getRequestUrl();
int index = requestUrl.indexOf("?");
if (index != -1){
requestUrl = requestUrl.substring(0, index);
}
// 白名單,設(shè)置需要的角色為null
for (String url : ignoreUrlsConfig.getUrls()) {
if (url.equals(requestUrl) || pathMatcher.match(url , requestUrl)) {
return null;
}
}
// 數(shù)據(jù)庫中所有的菜單
List<Permission> permissionList = permissionMapper.selectList(null);
if (CollectionUtils.isEmpty(permissionList)){
return null;
}
for (Permission permission : permissionList) {
// 與請求地址進(jìn)行匹配,獲取該url所對應(yīng)的權(quán)限
if (pathMatcher.match(permission.getUrl()+"/**", requestUrl)){
List<RolePermission> permissions = rolePermissionMapper.selectList(new LambdaQueryWrapper<RolePermission>().eq(RolePermission::getPermissionId, permission.getId()));
if (!CollectionUtils.isEmpty(permissions)){
Set<Integer> roleIdSet = permissions.stream().map(RolePermission::getRoleId).collect(Collectors.toSet());
List<Role> roleList = roleMapper.selectBatchIds(roleIdSet);
List<String> roleStringList = roleList.stream().map(Role::getCode).collect(Collectors.toList());
// 保存該url對應(yīng)角色權(quán)限信息
return SecurityConfig.createList(roleStringList.toArray(new String[roleStringList.size()]));
}
}
}
// 如果數(shù)據(jù)中沒有找到相應(yīng)url資源則為無權(quán)限訪問
return SecurityConfig.createList(ResultConstant.REQUEST_FORBIDDEN_ROLE);
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
7、權(quán)限認(rèn)證處理類:UrlAccessDecisionManager implements AccessDecisionManager,認(rèn)證失敗拋出:AccessDeniedException 異常
package com.example.security.url.url;
import com.example.security.url.constants.ResultConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* @author Deyou Kong
* @description 權(quán)限認(rèn)證處理類
* @date 2023/2/10 4:37 下午
*/
@Slf4j
public class UrlAccessDecisionManager implements AccessDecisionManager {
/**
*
* @param authentication
* @param o
* @param configAttributes URL所需要的角色權(quán)限列表:String[],UrlRoleNeedFilterInvocationSecurityMetadataSource.getAttributes返回的對象
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
log.info("UrlAccessDecisionManager --- > decide");
// 遍歷角色
for (ConfigAttribute configAttribute : configAttributes) {
// 當(dāng)前url請求需要的權(quán)限
String needRole = configAttribute.getAttribute();
if (needRole.equals(ResultConstant.REQUEST_FORBIDDEN_ROLE)){
throw new AccessDeniedException(ResultConstant.REQUEST_FORBIDDEN);
}
// 只要包含其中一個角色即可訪問
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException(ResultConstant.REQUEST_FORBIDDEN);
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
8、權(quán)限認(rèn)證失敗后的處理類:UrlAccessDeniedHandler implements AccessDeniedHandler
package com.example.security.url.url;
import com.example.security.url.common.result.CommonResult;
import com.example.security.url.constants.ResultConstant;
import com.example.security.url.utils.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 在實(shí)現(xiàn) AccessDecisionManager 接口中拋出 org.springframework.security.access.AccessDeniedException 異常會被這里捕獲
* @author Deyou Kong
* @description 權(quán)限認(rèn)證失敗處理類Handler
* @date 2023/2/10 4:53 下午
*/
@Slf4j
public class UrlAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.info("UrlAccessDeniedHandler權(quán)限認(rèn)證失敗處理類");
ResponseUtils.out(httpServletResponse, CommonResult.forbidden(e.getLocalizedMessage()));
}
}
9、核心配置SecurityConfig
package com.example.security.url.config;
import com.example.security.url.filter.LoginFilter;
import com.example.security.url.filter.SecurityFilter;
import com.example.security.url.filter.LoginFailHandlerEntryPoint;
import com.example.security.url.url.UrlAccessDecisionManager;
import com.example.security.url.url.UrlAccessDeniedHandler;
import com.example.security.url.property.IgnoreUrlsConfig;
import com.example.security.url.url.UrlFilterInvocationSecurityMetadataSource;
import com.example.security.url.utils.MD5PasswordEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
IgnoreUrlsConfig ignoreUrlsConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();
// 禁用CSRF 開啟跨域
http.csrf().disable().cors();
// 未登錄認(rèn)證異常
http.exceptionHandling().authenticationEntryPoint(loginFailHandlerEntryPoint());
// 登錄過后訪問無權(quán)限的接口時自定義403響應(yīng)內(nèi)容
http.exceptionHandling().accessDeniedHandler(urlAccessDeniedHandler());
// url權(quán)限認(rèn)證處理
registry.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
o.setAccessDecisionManager(urlAccessDecisionManager());
return o;
}
});
// OPTIONS(選項(xiàng)):查找適用于一個特定網(wǎng)址資源的通訊選擇。 在不需執(zhí)行具體的涉及數(shù)據(jù)傳輸?shù)膭幼髑闆r下, 允許客戶端來確定與資源相關(guān)的選項(xiàng)以及 / 或者要求, 或是一個服務(wù)器的性能
registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
// 自動登錄 - cookie儲存方式
registry.and().rememberMe();
// 其余所有請求都需要認(rèn)證
registry.anyRequest().authenticated();
// 防止iframe 造成跨域
registry.and().headers().frameOptions().disable();
// 自定義過濾器在登錄時認(rèn)證用戶名、密碼
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(securityFilter(), FilterSecurityInterceptor.class);
}
/**
* 忽略攔截url或靜態(tài)資源文件夾 - web.ignoring(): 會直接過濾該url - 將不會經(jīng)過Spring Security過濾器鏈
* http.permitAll(): 不會繞開springsecurity驗(yàn)證,相當(dāng)于是允許該路徑通過
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.GET,
"/favicon.ico",
"/*.html",
"/**/*.css",
"/**/*.js");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
/**
* 登錄過濾器
*/
@Bean
public LoginFilter loginFilter(){
return new LoginFilter();
}
/**
* 登錄失敗處理類
*/
@Bean
public LoginFailHandlerEntryPoint loginFailHandlerEntryPoint(){
return new LoginFailHandlerEntryPoint();
};
/**
* 獲取訪問url所需要的角色信息
*/
@Bean
public UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource(){
return new UrlFilterInvocationSecurityMetadataSource();
};
/**
* 認(rèn)證權(quán)限處理 - 將可以請求URL的角色權(quán)限與當(dāng)前登錄用戶的角色做對比,如果包含其中一個角色即可正常訪問
*/
@Bean
public UrlAccessDecisionManager urlAccessDecisionManager(){
return new UrlAccessDecisionManager();
};
/**
* 自定義訪問無權(quán)限接口時403響應(yīng)內(nèi)容
*/
@Bean
public UrlAccessDeniedHandler urlAccessDeniedHandler(){
return new UrlAccessDeniedHandler();
};
@Bean
public SecurityFilter securityFilter() {
return new SecurityFilter();
}
/**
* 密碼加密類
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new MD5PasswordEncoder();
}
}
其他工具類
1、自定義異常
package com.example.security.url.exception;
import lombok.Data;
/**
* @author Deyou Kong
* @description 登錄異常
* @date 2023/2/13 9:18 上午
*/
@Data
public class LoginException extends RuntimeException{
private String message;
public LoginException(String message){
this.message = message;
}
}
2、讀取配置文件配置
package com.example.security.url.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Data
@Component
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {
private List<String> urls;
}
3、MD5加密工具類
package com.example.security.url.utils;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @author Deyou Kong
* @description MD5算法
* @date 2023/2/10 7:16 下午
*/
public class MD5Utils {
/**
* 使用md5的算法進(jìn)行加密
*/
public static String encode(String plainText) {
byte[] secretBytes = null;
try {
secretBytes = MessageDigest.getInstance("md5").digest(
plainText.getBytes());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("沒有md5這個算法!");
}
String md5code = new BigInteger(1, secretBytes).toString(16);// 16進(jìn)制數(shù)字
// 如果生成數(shù)字未滿32位,需要前面補(bǔ)0
for (int i = 0; i < 32 - md5code.length(); i++) {
md5code = "0" + md5code;
}
return md5code;
}
}
4、MD5PasswordEncoder
package com.example.security.url.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Deyou Kong
* @description
* @date 2023/2/10 7:16 下午
*/
@Slf4j
public class MD5PasswordEncoder implements PasswordEncoder {
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
log.info("MD5PasswordEncoder的matches");
return encodedPassword.equals(MD5Utils.encode((String)rawPassword));
}
@Override
public String encode(CharSequence rawPassword) {
log.info("MD5PasswordEncoder的encode");
return MD5Utils.encode((String)rawPassword);
}
}
5、token工具類
package com.example.security.url.utils;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.example.security.url.constants.ResultConstant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.security.sasl.AuthenticationException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具類
* JWT token的格式:header.payload.signature
* header的格式(算法、token的類型):
* {"alg": "HS512","typ": "JWT"}
* payload的格式(用戶名、創(chuàng)建時間、生成時間):
* {"sub":"wang","created":1489079981393,"exp":1489684781}
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
*/
@Component
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenType}")
private String tokenType;
/**
* 根據(jù)負(fù)責(zé)生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)))
.compact();
}
/**
* 從token中獲取JWT中的負(fù)載
*/
private Claims getClaimsFromToken(String token) throws AuthenticationException {
Claims claims = null;
try {
claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)))
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e){
claims =e.getClaims();
} catch (Exception e) {
LOGGER.error("獲取token:【{}】中的JWT負(fù)載失敗:【{}】", token, e.getMessage());
}
return claims;
}
/**
* 生成token的過期時間
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 從token中獲取登錄用戶名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 驗(yàn)證token中的用戶是否還有效
*
* @param token 客戶端傳入的token
* @param userDetails 從數(shù)據(jù)庫中查詢出來的用戶信息
*/
public boolean validateToken(String token, UserDetails userDetails) throws AuthenticationException {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判斷token是否已經(jīng)失效
*/
public boolean isTokenExpired(String token) throws AuthenticationException {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 從token中獲取過期時間
*/
private Date getExpiredDateFromToken(String token) throws AuthenticationException {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根據(jù)用戶信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 當(dāng)原來的token沒過期時是可以刷新的
*
* @param oldToken 帶tokenHead的token
*/
public String refreshHeadToken(String oldToken) throws AuthenticationException {
if (StrUtil.isEmpty(oldToken)) {
return null;
}
String token = oldToken.substring(tokenType.length());
if (StrUtil.isEmpty(token)) {
return null;
}
//token校驗(yàn)不通過
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return null;
}
//如果token已經(jīng)過期,不支持刷新
if (isTokenExpired(token)) {
return null;
}
//如果token在30分鐘之內(nèi)剛刷新過,返回原token
if (tokenRefreshJustBefore(token, 30 * 60)) {
return token;
} else {
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
/**
* 判斷token在指定時間內(nèi)是否剛剛刷新過
*
* @param token 原token
* @param time 指定時間(秒)
*/
private boolean tokenRefreshJustBefore(String token, int time) throws AuthenticationException {
Claims claims = getClaimsFromToken(token);
Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
Date refreshDate = new Date();
//刷新時間在創(chuàng)建時間的指定時間內(nèi)
if (refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time))) {
return true;
}
return false;
}
}6、輸入流工具類
package com.example.security.url.utils;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.security.url.common.result.CommonResult;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Deyou Kong
* @description 響應(yīng)處理類
* @date 2023/2/9 3:33 下午
*/
public class ResponseUtils {
public static void out(HttpServletResponse response, CommonResult result) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONObject.toJSONString(result, SerializerFeature.WriteMapNullValue)); // 保留值為null的字段
response.getWriter().flush();
}
}
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java位集合之BitMap實(shí)現(xiàn)和應(yīng)用詳解
這篇文章主要介紹了Java位集合之BitMap實(shí)現(xiàn)和應(yīng)用的相關(guān)資料,BitMap是一種高效的數(shù)據(jù)結(jié)構(gòu),適用于快速排序、去重和查找等操作,通過簡單的數(shù)組和位運(yùn)算,可以在Java中實(shí)現(xiàn)BitMap,從而節(jié)省存儲空間并提高性能,需要的朋友可以參考下2024-12-12
java實(shí)現(xiàn)一個掃描包的工具類實(shí)例代碼
很多框架,比如springmvc,mybatis等使用注解,為了處理注解,必然要對包進(jìn)行掃描,所以下面這篇文章主要給大家分享介紹了關(guān)于利用java如何實(shí)現(xiàn)一個掃描包的工具類,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-10-10
Java spring事務(wù)及事務(wù)不生效的原因詳解
在日常編碼過程中常常涉及到事務(wù),在前兩天看到一篇文章提到了Spring事務(wù),那么在此總結(jié)下在Spring環(huán)境下事務(wù)失效的幾種原因2021-09-09
Java報(bào)錯:java.util.concurrent.ExecutionException的解決辦法
在Java并發(fā)編程中,我們經(jīng)常使用java.util.concurrent包提供的工具來管理和協(xié)調(diào)多個線程的執(zhí)行,va并發(fā)編程中,然而,在使用這些工具時,可能會遇到各種各樣的異常,其中之一就是java.util.concurrent.ExecutionException,本文將詳細(xì)分析這種異常的背景、可能的原因2024-09-09
Java實(shí)現(xiàn)作業(yè)調(diào)度的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用Java實(shí)現(xiàn)SJF算法調(diào)度,要求測試數(shù)據(jù)可以隨即輸入或從文件中讀入,文中的示例代碼講解詳細(xì),需要的可以參考一下2023-04-04

