Spring Security实现动态路由权限控制方式
作者:DeyouKong
Spring Security实现动态路由权限控制
主要步骤如下:
- 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(); } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。