Spring Security實(shí)現(xiàn)動(dòng)態(tài)路由權(quán)限控制方式
Spring Security實(shí)現(xiàn)動(dòng)態(tài)路由權(quán)限控制
主要步驟如下:
- 1、SecurityUser implements UserDetails 接口中的方法
- 2、自定義認(rèn)證:UserDetailsServiceImpl implements UserDetailsService
- 3、添加登錄過(guò)濾器LoginFilter extends OncePerRequestFilter
每次訪問(wèn)接口都會(huì)經(jīng)過(guò)此,我們可以在這里記錄請(qǐng)求參數(shù)、響應(yīng)內(nèi)容,或者處理前后端分離情況下, 以token換用戶權(quán)限信息,token是否過(guò)期,請(qǐng)求頭類型是否正確,防止非法請(qǐng)求等等
- 4、動(dòng)態(tài)權(quán)限過(guò)濾器,用于實(shí)現(xiàn)基于路徑的動(dòng)態(tài)權(quán)限過(guò)濾:SecurityFilter extends AbstractSecurityInterceptor implements Filter
- 5、未登錄訪問(wèn)控制類:AdminAuthenticationEntryPoint implements AuthenticationEntryPoint
- 6、獲取訪問(wèn)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){ // 拋出異常,會(huì)被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); // 判斷用戶有沒(méi)有角色,沒(méi)有角色,直接返回空列表 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、添加登錄過(guò)濾器LoginFilter extends OncePerRequestFilter
每次訪問(wèn)接口都會(huì)經(jīng)過(guò)此,我們可以在這里記錄請(qǐng)求參數(shù)、響應(yīng)內(nèi)容等日志,或者處理前后端分離情況下,以token換用戶權(quán)限信息,token是否過(guò)期,請(qǐng)求頭類型是否正確,防止非法請(qǐng)求等等
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; /** * 請(qǐng)求的HttpServletRequest流只能讀一次,下一次就不能讀取了, * 因此這里要使用自定義的MultiReadHttpServletRequest工具解決流只能讀一次的問(wèn)題 * * @author Deyou Kong * @description 用戶登錄鑒權(quán)過(guò)濾器 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,請(qǐng)求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ù)庫(kù)中查詢到數(shù)據(jù),開(kāi)始進(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、動(dòng)態(tài)權(quán)限過(guò)濾器,用于實(shí)現(xiàn)基于路徑的動(dòng)態(tài)權(quán)限過(guò)濾: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; /** * 動(dòng)態(tài)權(quán)限過(guò)濾器,用于實(shí)現(xiàn)基于路徑的動(dòng)態(tài)權(quán)限過(guò)濾 */ @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動(dòng)態(tài)權(quán)限過(guò)濾器,用于實(shí)現(xiàn)基于路徑的動(dòng)態(tài)權(quán)限過(guò)濾"); /** * 仿照OncePerRequestFilter,解決Filter執(zhí)行兩次的問(wèn)題 * 執(zhí)行兩次原因:SecurityConfig中,@Bean和addFilter相當(dāng)于向容器注入了兩次 * 解決辦法:1是去掉@Bean,但Filter中若有引用注入容器的其它資源,則會(huì)報(bào)錯(cuò) * 2就是request中保存一個(gè)Attribute來(lái)判斷該請(qǐng)求是否已執(zhí)行過(guò) */ 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請(qǐng)求直接放行 if (request.getMethod().equals(HttpMethod.OPTIONS.toString())) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } //白名單請(qǐng)求直接放行 PathMatcher pathMatcher = new AntPathMatcher(); for (String path : ignoreUrlsConfig.getUrls()) { if (pathMatcher.match(contextPath + path, request.getRequestURI())) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); return; } } //此處會(huì)調(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對(duì)象"); return urlFilterInvocationSecurityMetadataSource; } protected String getAlreadyFilteredAttributeName() { return this.getClass().getName() + ".FILTERED"; } }
5、未登錄訪問(wèn)控制類: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 異常都會(huì)被此類捕獲 * @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、獲取訪問(wèn)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 訪問(wèn)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: 儲(chǔ)存請(qǐng)求url信息 * @return: null:標(biāo)識(shí)不需要任何權(quán)限都可以訪問(wèn) */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { log.info("UrlFilterInvocationSecurityMetadataSource獲取請(qǐng)求URL所需角色"); // 獲取當(dāng)前請(qǐ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ù)庫(kù)中所有的菜單 List<Permission> permissionList = permissionMapper.selectList(null); if (CollectionUtils.isEmpty(permissionList)){ return null; } for (Permission permission : permissionList) { // 與請(qǐng)求地址進(jìn)行匹配,獲取該url所對(duì)應(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對(duì)應(yīng)角色權(quán)限信息 return SecurityConfig.createList(roleStringList.toArray(new String[roleStringList.size()])); } } } // 如果數(shù)據(jù)中沒(méi)有找到相應(yīng)url資源則為無(wú)權(quán)限訪問(wè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返回的對(duì)象 * @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請(qǐng)求需要的權(quán)限 String needRole = configAttribute.getAttribute(); if (needRole.equals(ResultConstant.REQUEST_FORBIDDEN_ROLE)){ throw new AccessDeniedException(ResultConstant.REQUEST_FORBIDDEN); } // 只要包含其中一個(gè)角色即可訪問(wèn) 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 異常會(huì)被這里捕獲 * @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 開(kāi)啟跨域 http.csrf().disable().cors(); // 未登錄認(rèn)證異常 http.exceptionHandling().authenticationEntryPoint(loginFailHandlerEntryPoint()); // 登錄過(guò)后訪問(wèn)無(wú)權(quán)限的接口時(shí)自定義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)):查找適用于一個(gè)特定網(wǎng)址資源的通訊選擇。 在不需執(zhí)行具體的涉及數(shù)據(jù)傳輸?shù)膭?dòng)作情況下, 允許客戶端來(lái)確定與資源相關(guān)的選項(xiàng)以及 / 或者要求, 或是一個(gè)服務(wù)器的性能 registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll(); // 自動(dòng)登錄 - cookie儲(chǔ)存方式 registry.and().rememberMe(); // 其余所有請(qǐng)求都需要認(rèn)證 registry.anyRequest().authenticated(); // 防止iframe 造成跨域 registry.and().headers().frameOptions().disable(); // 自定義過(guò)濾器在登錄時(shí)認(rèn)證用戶名、密碼 http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(securityFilter(), FilterSecurityInterceptor.class); } /** * 忽略攔截url或靜態(tài)資源文件夾 - web.ignoring(): 會(huì)直接過(guò)濾該url - 將不會(huì)經(jīng)過(guò)Spring Security過(guò)濾器鏈 * http.permitAll(): 不會(huì)繞開(kāi)springsecurity驗(yàn)證,相當(dāng)于是允許該路徑通過(guò) * @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()); } /** * 登錄過(guò)濾器 */ @Bean public LoginFilter loginFilter(){ return new LoginFilter(); } /** * 登錄失敗處理類 */ @Bean public LoginFailHandlerEntryPoint loginFailHandlerEntryPoint(){ return new LoginFailHandlerEntryPoint(); }; /** * 獲取訪問(wèn)url所需要的角色信息 */ @Bean public UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource(){ return new UrlFilterInvocationSecurityMetadataSource(); }; /** * 認(rèn)證權(quán)限處理 - 將可以請(qǐng)求URL的角色權(quán)限與當(dāng)前登錄用戶的角色做對(duì)比,如果包含其中一個(gè)角色即可正常訪問(wèn) */ @Bean public UrlAccessDecisionManager urlAccessDecisionManager(){ return new UrlAccessDecisionManager(); }; /** * 自定義訪問(wèn)無(wú)權(quán)限接口時(shí)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("沒(méi)有md5這個(gè)算法!"); } 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)建時(shí)間、生成時(shí)間): * {"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的過(guò)期時(shí)間 */ 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ù)庫(kù)中查詢出來(lái)的用戶信息 */ 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中獲取過(guò)期時(shí)間 */ 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)原來(lái)的token沒(méi)過(guò)期時(shí)是可以刷新的 * * @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)不通過(guò) Claims claims = getClaimsFromToken(token); if (claims == null) { return null; } //如果token已經(jīng)過(guò)期,不支持刷新 if (isTokenExpired(token)) { return null; } //如果token在30分鐘之內(nèi)剛刷新過(guò),返回原token if (tokenRefreshJustBefore(token, 30 * 60)) { return token; } else { claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } } /** * 判斷token在指定時(shí)間內(nèi)是否剛剛刷新過(guò) * * @param token 原token * @param time 指定時(shí)間(秒) */ 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(); //刷新時(shí)間在創(chuàng)建時(shí)間的指定時(shí)間內(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é)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java位集合之BitMap實(shí)現(xiàn)和應(yīng)用詳解
這篇文章主要介紹了Java位集合之BitMap實(shí)現(xiàn)和應(yīng)用的相關(guān)資料,BitMap是一種高效的數(shù)據(jù)結(jié)構(gòu),適用于快速排序、去重和查找等操作,通過(guò)簡(jiǎn)單的數(shù)組和位運(yùn)算,可以在Java中實(shí)現(xiàn)BitMap,從而節(jié)省存儲(chǔ)空間并提高性能,需要的朋友可以參考下2024-12-12java實(shí)現(xiàn)一個(gè)掃描包的工具類實(shí)例代碼
很多框架,比如springmvc,mybatis等使用注解,為了處理注解,必然要對(duì)包進(jìn)行掃描,所以下面這篇文章主要給大家分享介紹了關(guān)于利用java如何實(shí)現(xiàn)一個(gè)掃描包的工具類,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-10-10Java spring事務(wù)及事務(wù)不生效的原因詳解
在日常編碼過(guò)程中常常涉及到事務(wù),在前兩天看到一篇文章提到了Spring事務(wù),那么在此總結(jié)下在Spring環(huán)境下事務(wù)失效的幾種原因2021-09-09Java報(bào)錯(cuò):java.util.concurrent.ExecutionException的解決辦法
在Java并發(fā)編程中,我們經(jīng)常使用java.util.concurrent包提供的工具來(lái)管理和協(xié)調(diào)多個(gè)線程的執(zhí)行,va并發(fā)編程中,然而,在使用這些工具時(shí),可能會(huì)遇到各種各樣的異常,其中之一就是java.util.concurrent.ExecutionException,本文將詳細(xì)分析這種異常的背景、可能的原因2024-09-09springboot jpa 延遲加載問(wèn)題的2種解決
這篇文章主要介紹了springboot jpa 延遲加載問(wèn)題的2種解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07Java實(shí)現(xiàn)作業(yè)調(diào)度的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用Java實(shí)現(xiàn)SJF算法調(diào)度,要求測(cè)試數(shù)據(jù)可以隨即輸入或從文件中讀入,文中的示例代碼講解詳細(xì),需要的可以參考一下2023-04-04