开发者

Spring Security实现动态路由权限控制方式

开发者 https://www.devze.com 2024-08-15 10:31 出处:网络 作者: DeyouKong
目录Spring Security实现动态路由权限控制代码实现其他工具类总结Spring Security实现动态路由权限控制
目录
  • Spring Security实现动态路由权限控制
    • 代码实现
    • 其他工具类
  • 总结

    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

    Spring Security实现动态路由权限控制方式

    代码实现

    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.inandroidtercept.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 clpythonass 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.springframeworkhttp://www.devze.com.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;
                }
    javascript        });
    
            // OPTIONS(选项):查找适用于一个特定网址资源的通讯选择。 在不需执行具体的涉及数据传输的动作情况下, 允许客户端来确定与资源相关的选项以及 / 或者要求, 或是一个服务器的性能
            registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
            // 自动登录 - cookie储存方式
            registry.and().rememberMe();
            // 其余所有请求都需要认证
            registry.anyRelLTQXNlZnquest().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();
        }
    }
    

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号