Spring Security实现动态路由权限控制
前面已经学了security 的入门,不懂可以看下springboot整合spring security + MybatisPlus入门
本章讲解实现动态路由权限控制
主要步骤如下:
- 1、SecurityUser implements UserDetails 接口中的方法
- 2、自定义认证:UserDetailsServiceImpl implements UserDetailsService
- 3、添加登录过滤器LoginFilter extends OncePerRequestFilter
每次访问接口都会经过此,我们可以在这里记录请求参数、响应内容,或者处理前后端分离情况下, 以token换用户权限信息,token是否过期,请求头类型是否正确,防止非法请求等等
- 4、动态权限过滤器,用于实现基于路径的动态权限过滤:SecurityFilter extends AbstractSecurityInterceptor implements Filter
- 5、未登录访问控制类:AdminAuthenticationEntryPoint implements AuthenticationEntryPoint
- 6、获取访问URL所需要的角色信息类:UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource
- 7、权限认证处理类:UrlAccessDecisionManager implements AccessDecisionManager,
认证失败抛出:AccessDeniedException 异常 - 8、权限认证失败后的处理类:UrlAccessDeniedHandler implements AccessDeniedHandler
- 9、核心配置SecurityConfig
代码实现
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验证用户
* @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;
}
}
/**
* 获取当前用户所具有的角色
* @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、自定义认证: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实现类");
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(queryWrapper);
//如果用户被禁用,则不再查询权限表
if (user == null){
// 抛出异常,会被LoginFailHandlerEntryPoint捕获
throw new UsernameNotFoundException(ResultConstant.USER_NOT_EXIST);
//return null;
}
return new SecurityUser(user, getUserRoles(user.getId()));
}
/**
* 根据用户id获取角色权限信息
*
* @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
每次访问接口都会经过此,我们可以在这里记录请求参数、响应内容等日志,或者处理前后端分离情况下,以token换用户权限信息,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 用户登录鉴权过滤器 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;
}
}
// 验证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 中的用户在数据库中查询到数据,开始进行密码验证
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、动态权限过滤器,用于实现基于路径的动态权限过滤: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;
/**
* 动态权限过滤器,用于实现基于路径的动态权限过滤
*/
@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动态权限过滤器,用于实现基于路径的动态权限过滤");
/**
* 仿照OncePerRequestFilter,解决Filter执行两次的问题
* 执行两次原因:SecurityConfig中,@Bean和addFilter相当于向容器注入了两次
* 解决办法:1是去掉@Bean,但Filter中若有引用注入容器的其它资源,则会报错
* 2就是request中保存一个Attribute来判断该请求是否已执行过
*/
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;
}
}
//此处会调用AccessDecisionManager中的decide方法进行鉴权操作
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;
/**
* 在实现 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需要的角色权限
* @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所需要的用户权限信息
*
* @param object: 储存请求url信息
* @return: null:标识不需要任何权限都可以访问
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
log.info("UrlFilterInvocationSecurityMetadataSource获取请求URL所需角色");
// 获取当前请求url
String requestUrl = ((FilterInvocation) object).getRequestUrl();
int index = requestUrl.indexOf("?");
if (index != -1){
requestUrl = requestUrl.substring(0, index);
}
// 白名单,设置需要的角色为null
for (String url : ignoreUrlsConfig.getUrls()) {
if (url.equals(requestUrl) || pathMatcher.match(url , requestUrl)) {
return null;
}
}
// 数据库中所有的菜单
List<Permission> permissionList = permissionMapper.selectList(null);
if (CollectionUtils.isEmpty(permissionList)){
return null;
}
for (Permission permission : permissionList) {
// 与请求地址进行匹配,获取该url所对应的权限
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对应角色权限信息
return SecurityConfig.createList(roleStringList.toArray(new String[roleStringList.size()]));
}
}
}
// 如果数据中没有找到相应url资源则为无权限访问
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、权限认证处理类:UrlAccessDecisionManager implements AccessDecisionManager,认证失败抛出: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 权限认证处理类
* @date 2023/2/10 4:37 下午
*/
@Slf4j
public class UrlAccessDecisionManager implements AccessDecisionManager {
/**
*
* @param authentication
* @param o
* @param configAttributes URL所需要的角色权限列表: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) {
// 当前url请求需要的权限
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、权限认证失败后的处理类: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;
/**
* 在实现 AccessDecisionManager 接口中抛出 org.springframework.security.access.AccessDeniedException 异常会被这里捕获
* @author Deyou Kong
* @description 权限认证失败处理类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权限认证失败处理类");
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();
// 未登录认证异常
http.exceptionHandling().authenticationEntryPoint(loginFailHandlerEntryPoint());
// 登录过后访问无权限的接口时自定义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;
}
});
// OPTIONS(选项):查找适用于一个特定网址资源的通讯选择。 在不需执行具体的涉及数据传输的动作情况下, 允许客户端来确定与资源相关的选项以及 / 或者要求, 或是一个服务器的性能
registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
// 自动登录 - cookie储存方式
registry.and().rememberMe();
// 其余所有请求都需要认证
registry.anyRequest().authenticated();
// 防止iframe 造成跨域
registry.and().headers().frameOptions().disable();
// 自定义过滤器在登录时认证用户名、密码
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(securityFilter(), FilterSecurityInterceptor.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");
}
@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();
};
/**
* 认证权限处理 - 将可以请求URL的角色权限与当前登录用户的角色做对比,如果包含其中一个角色即可正常访问
*/
@Bean
public UrlAccessDecisionManager urlAccessDecisionManager(){
return new UrlAccessDecisionManager();
};
/**
* 自定义访问无权限接口时403响应内容
*/
@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的算法进行加密
*/
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进制数字
// 如果生成数字未满32位,需要前面补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的格式(用户名、创建时间、生成时间):
* {"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;
/**
* 根据负责生成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中的负载
*/
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负载失败:【{}】", 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;
}
/**
* 验证token中的用户是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) throws AuthenticationException {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否已经失效
*/
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();
}
/**
* 根据用户信息生成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);
}
/**
* 当原来的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校验不通过
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return null;
}
//如果token已经过期,不支持刷新
if (isTokenExpired(token)) {
return null;
}
//如果token在30分钟之内刚刷新过,返回原token
if (tokenRefreshJustBefore(token, 30 * 60)) {
return token;
} else {
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
/**
* 判断token在指定时间内是否刚刚刷新过
*
* @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();
//刷新时间在创建时间的指定时间内
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 响应处理类
* @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();
}
}