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

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

前面已经学了security 的入门,不懂可以看下springboot整合spring security + MybatisPlus入门
本章讲解实现动态路由权限控制

主要步骤如下:

  • 1、SecurityUser implements UserDetails 接口中的方法
  • 2、自定义认证:UserDetailsServiceImpl implements UserDetailsService
  • 3、添加登录过滤器LoginFilter extends OncePerRequestFilter

每次访问接口都会经过此,我们可以在这里记录请求参数、响应内容,或者处理前后端分离情况下, 以token换用户权限信息,token是否过期,请求头类型是否正确,防止非法请求等等

  • 4、动态权限过滤器,用于实现基于路径的动态权限过滤:SecurityFilter extends AbstractSecurityInterceptor implements Filter
  • 5、未登录访问控制类:AdminAuthenticationEntryPoint implements AuthenticationEntryPoint
  • 6、获取访问URL所需要的角色信息类:UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource
  • 7、权限认证处理类:UrlAccessDecisionManager implements AccessDecisionManager,
    认证失败抛出:AccessDeniedException 异常
  • 8、权限认证失败后的处理类:UrlAccessDeniedHandler implements AccessDeniedHandler
  • 9、核心配置SecurityConfig

在这里插入图片描述


代码实现

1、SecurityUser implements UserDetails 接口中的方法
package com.example.security.url.entity;

import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author Deyou Kong
 * @description security验证用户
 * @date 2023/2/9 3:01 下午
 */

@Data
@Slf4j
@ToString
public class SecurityUser implements UserDetails {

    /**
     * 用户信息
     */
    private User user;

    /**
     * 用户拥有的角色列表
     */
    private List<Role> roles;

    public SecurityUser() { }

    public SecurityUser(User user) {
        if (user != null) {
            this.user = user;
        }
    }

    public SecurityUser(User user, List<Role> roleList) {
        if (user != null) {
            this.user = user;
            this.roles = roleList;
        }
    }

    /**
     * 获取当前用户所具有的角色
     * @return 返回角色列表 List<Role.getCode()>
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        if (!CollectionUtils.isEmpty(this.roles)) {
            for (Role role : this.roles) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getCode());
                authorities.add(authority);
            }
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return user.getStatus() == 1 ? true: false;
    }
}

2、自定义认证:UserDetailsServiceImpl implements UserDetailsService
package com.example.security.url.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.security.url.constants.ResultConstant;
import com.example.security.url.dao.RoleMapper;
import com.example.security.url.dao.UserMapper;
import com.example.security.url.dao.UserRoleMapper;
import com.example.security.url.entity.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    UserMapper userMapper;

    @Resource
    UserRoleMapper userRoleMapper;

    @Resource
    RoleMapper roleMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("UserDetailsService实现类");
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, username);
        User user = userMapper.selectOne(queryWrapper);
        //如果用户被禁用,则不再查询权限表
        if (user == null){
            // 抛出异常,会被LoginFailHandlerEntryPoint捕获
            throw new UsernameNotFoundException(ResultConstant.USER_NOT_EXIST);
            //return null;
        }
        return new SecurityUser(user, getUserRoles(user.getId()));
    }

    /**
     * 根据用户id获取角色权限信息
     *
     * @param userId
     * @return
     */
    private List<Role> getUserRoles(Integer userId) {
        LambdaQueryWrapper<UserRole> userRoleLambdaQueryWrapper = new LambdaQueryWrapper<>();
        userRoleLambdaQueryWrapper.eq(UserRole::getUserId, userId);
        List<UserRole> userRoles = userRoleMapper.selectList(userRoleLambdaQueryWrapper);
        // 判断用户有没有角色,没有角色,直接返回空列表
        if (CollectionUtils.isEmpty(userRoles)){
            return new ArrayList<>();
        }
        Set<Integer> roleIdSet = userRoles.stream().map(UserRole::getRoleId).collect(Collectors.toSet());
        List<Role> roles = roleMapper.selectBatchIds(roleIdSet);
        if (CollectionUtils.isEmpty(roles)){
            return new ArrayList<>();
        }
        return roles;
    }

}

3、添加登录过滤器LoginFilter extends OncePerRequestFilter

每次访问接口都会经过此,我们可以在这里记录请求参数、响应内容等日志,或者处理前后端分离情况下,以token换用户权限信息,token是否过期,请求头类型是否正确,防止非法请求等等

package com.example.security.url.filter;

import com.example.security.url.common.result.CommonResult;
import com.example.security.url.exception.LoginException;
import com.example.security.url.property.IgnoreUrlsConfig;
import com.example.security.url.constants.ResultConstant;
import com.example.security.url.utils.JwtTokenUtil;
import com.example.security.url.utils.ResponseUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 请求的HttpServletRequest流只能读一次,下一次就不能读取了,
 * 因此这里要使用自定义的MultiReadHttpServletRequest工具解决流只能读一次的问题
 *
 * @author Deyou Kong
 * @description 用户登录鉴权过滤器 filter
 * @date 2023/2/10 2:25 下午
 */

@Slf4j
public class LoginFilter extends OncePerRequestFilter {

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    IgnoreUrlsConfig ignoreUrlsConfig;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.tokenType}")
    private String tokenType;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        log.info("LoginFilter -> doFilterInternal,请求URL:{}", requestURI);
        // 如果requestURI在白名单中直接放行
        try {
            PathMatcher pathMatcher = new AntPathMatcher();
            for (String url : ignoreUrlsConfig.getUrls()) {
                String requestUrl = contextPath + url;
                if (pathMatcher.match((requestUrl), requestURI)) {
                    chain.doFilter(request, response);
                    return;
                }
            }

            // 验证token
            String token = request.getHeader(tokenHeader);
            if (StringUtils.isAllBlank(token)){
                throw new LoginException(ResultConstant.NOT_TOKEN);
            }
            if (!token.startsWith(tokenType)){
                throw new LoginException(ResultConstant.TOKEN_REG_FAIL);
            }
            String authToken = token.substring(tokenType.length());
            if (jwtTokenUtil.isTokenExpired(authToken)){
                throw new LoginException(ResultConstant.TOKEN_INVALID);
            }

            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            //if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (username != null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (userDetails != null) {
                    // token 中的用户在数据库中查询到数据,开始进行密码验证
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    chain.doFilter(request, response);
                    return;
                }
            }
        } catch (LoginException e) {
            CommonResult<String> result = CommonResult.loginFailed(e.getMessage());
            ResponseUtils.out(response, result);
        }catch (Exception e){
            e.printStackTrace();
            CommonResult<String> result = CommonResult.loginFailed(ResultConstant.SYS_ERROR);
            ResponseUtils.out(response, result);
            return ;
        }

    }
}

4、动态权限过滤器,用于实现基于路径的动态权限过滤:SecurityFilter extends AbstractSecurityInterceptor implements Filter
package com.example.security.url.filter;

import com.example.security.url.url.UrlAccessDecisionManager;
import com.example.security.url.property.IgnoreUrlsConfig;
import com.example.security.url.url.UrlFilterInvocationSecurityMetadataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 动态权限过滤器,用于实现基于路径的动态权限过滤
 */

@Slf4j
public class SecurityFilter extends AbstractSecurityInterceptor implements Filter {

    @Resource
    private UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;

    @Resource
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Resource
    public void setAccessDecisionManager(UrlAccessDecisionManager urlAccessDecisionManager) {
        super.setAccessDecisionManager(urlAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        log.info("SecurityFilter动态权限过滤器,用于实现基于路径的动态权限过滤");

        /**
         * 仿照OncePerRequestFilter,解决Filter执行两次的问题
         * 执行两次原因:SecurityConfig中,@Bean和addFilter相当于向容器注入了两次
         * 解决办法:1是去掉@Bean,但Filter中若有引用注入容器的其它资源,则会报错
         *         2就是request中保存一个Attribute来判断该请求是否已执行过
         */
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
        if (hasAlreadyFilteredAttribute) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }
        request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

        //OPTIONS请求直接放行
        if (request.getMethod().equals(HttpMethod.OPTIONS.toString())) {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }
        //白名单请求直接放行
        PathMatcher pathMatcher = new AntPathMatcher();
        for (String path : ignoreUrlsConfig.getUrls()) {
            if (pathMatcher.match(contextPath + path, request.getRequestURI())) {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                return;
            }
        }

        //此处会调用AccessDecisionManager中的decide方法进行鉴权操作
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {
        urlFilterInvocationSecurityMetadataSource.clearDataSource();
    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public UrlFilterInvocationSecurityMetadataSource obtainSecurityMetadataSource() {
        log.info("SecurityFilter返回UrlFilterInvocationSecurityMetadataSource对象");
        return urlFilterInvocationSecurityMetadataSource;
    }

    protected String getAlreadyFilteredAttributeName() {
        return this.getClass().getName() + ".FILTERED";
    }
}

5、未登录访问控制类:AdminAuthenticationEntryPoint implements AuthenticationEntryPoint
package com.example.security.url.filter;

import com.alibaba.fastjson.JSON;
import com.example.security.url.common.result.CommonResult;
import com.example.security.url.utils.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;


import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 在实现 UserDetailsService 接口的类中抛出 org.springframework.security.core.userdetails.UsernameNotFoundException 异常都会被此类捕获
 * @author Deyou Kong
 * @description 登录失败处理类/未登录,
 * @date 2023/2/10 2:19 下午
 */

@Slf4j
public class LoginFailHandlerEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.warn("LoginFailHandlerEntryPoint 登录失败处理类");
        ResponseUtils.out(response, CommonResult.loginFailed(authException.getLocalizedMessage()));
    }
}

ResponseUtils 工具类文末附上

6、获取访问URL所需要的角色信息类:UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource
package com.example.security.url.url;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.security.url.property.IgnoreUrlsConfig;
import com.example.security.url.constants.ResultConstant;
import com.example.security.url.dao.PermissionMapper;
import com.example.security.url.dao.RoleMapper;
import com.example.security.url.dao.RolePermissionMapper;
import com.example.security.url.entity.Permission;
import com.example.security.url.entity.Role;
import com.example.security.url.entity.RolePermission;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author Deyou Kong
 * @description 访问URL需要的角色权限
 * @date 2023/2/10 4:19 下午
 */


@Slf4j
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    /**
     * 正则匹配匹配
     */
    AntPathMatcher pathMatcher = new AntPathMatcher();

    @Resource
    PermissionMapper permissionMapper;

    @Resource
    RolePermissionMapper rolePermissionMapper;

    @Resource
    RoleMapper roleMapper;

    @Resource
    IgnoreUrlsConfig ignoreUrlsConfig;

    private List<ConfigAttribute> allConfigAttributes;

    public void clearDataSource() {
        allConfigAttributes.clear();
        allConfigAttributes = null;
    }

    /***
     * 返回该url所需要的用户权限信息
     *
     * @param object: 储存请求url信息
     * @return: null:标识不需要任何权限都可以访问
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        log.info("UrlFilterInvocationSecurityMetadataSource获取请求URL所需角色");
        // 获取当前请求url
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        int index = requestUrl.indexOf("?");
        if (index != -1){
            requestUrl = requestUrl.substring(0, index);
        }
        // 白名单,设置需要的角色为null
        for (String url : ignoreUrlsConfig.getUrls()) {
            if (url.equals(requestUrl) || pathMatcher.match(url , requestUrl)) {
                return null;
            }
        }
        // 数据库中所有的菜单
        List<Permission> permissionList = permissionMapper.selectList(null);
        if (CollectionUtils.isEmpty(permissionList)){
            return null;
        }
        for (Permission permission : permissionList) {
            // 与请求地址进行匹配,获取该url所对应的权限
            if (pathMatcher.match(permission.getUrl()+"/**", requestUrl)){
                List<RolePermission> permissions = rolePermissionMapper.selectList(new LambdaQueryWrapper<RolePermission>().eq(RolePermission::getPermissionId, permission.getId()));
                if (!CollectionUtils.isEmpty(permissions)){
                    Set<Integer> roleIdSet = permissions.stream().map(RolePermission::getRoleId).collect(Collectors.toSet());
                    List<Role> roleList = roleMapper.selectBatchIds(roleIdSet);
                    List<String> roleStringList = roleList.stream().map(Role::getCode).collect(Collectors.toList());
                    // 保存该url对应角色权限信息
                    return SecurityConfig.createList(roleStringList.toArray(new String[roleStringList.size()]));
                }
            }
        }
        // 如果数据中没有找到相应url资源则为无权限访问
        return SecurityConfig.createList(ResultConstant.REQUEST_FORBIDDEN_ROLE);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

7、权限认证处理类:UrlAccessDecisionManager implements AccessDecisionManager,认证失败抛出:AccessDeniedException 异常
package com.example.security.url.url;

import com.example.security.url.constants.ResultConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * @author Deyou Kong
 * @description 权限认证处理类
 * @date 2023/2/10 4:37 下午
 */

@Slf4j
public class UrlAccessDecisionManager implements AccessDecisionManager {
    /**
     *
     * @param authentication
     * @param o
     * @param configAttributes  URL所需要的角色权限列表:String[],UrlRoleNeedFilterInvocationSecurityMetadataSource.getAttributes返回的对象
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        log.info("UrlAccessDecisionManager --- > decide");
        // 遍历角色
        for (ConfigAttribute configAttribute : configAttributes) {
            // 当前url请求需要的权限
            String needRole = configAttribute.getAttribute();
            if (needRole.equals(ResultConstant.REQUEST_FORBIDDEN_ROLE)){
                throw new AccessDeniedException(ResultConstant.REQUEST_FORBIDDEN);
            }
            // 只要包含其中一个角色即可访问
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException(ResultConstant.REQUEST_FORBIDDEN);
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

8、权限认证失败后的处理类:UrlAccessDeniedHandler implements AccessDeniedHandler
package com.example.security.url.url;

import com.example.security.url.common.result.CommonResult;
import com.example.security.url.constants.ResultConstant;
import com.example.security.url.utils.ResponseUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 在实现 AccessDecisionManager 接口中抛出 org.springframework.security.access.AccessDeniedException 异常会被这里捕获
 * @author Deyou Kong
 * @description 权限认证失败处理类Handler
 * @date 2023/2/10 4:53 下午
 */

@Slf4j
public class UrlAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        log.info("UrlAccessDeniedHandler权限认证失败处理类");
        ResponseUtils.out(httpServletResponse, CommonResult.forbidden(e.getLocalizedMessage()));
    }
}

9、核心配置SecurityConfig
package com.example.security.url.config;

import com.example.security.url.filter.LoginFilter;
import com.example.security.url.filter.SecurityFilter;
import com.example.security.url.filter.LoginFailHandlerEntryPoint;
import com.example.security.url.url.UrlAccessDecisionManager;
import com.example.security.url.url.UrlAccessDeniedHandler;
import com.example.security.url.property.IgnoreUrlsConfig;
import com.example.security.url.url.UrlFilterInvocationSecurityMetadataSource;
import com.example.security.url.utils.MD5PasswordEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    IgnoreUrlsConfig ignoreUrlsConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.antMatcher("/**").authorizeRequests();

        // 禁用CSRF 开启跨域
        http.csrf().disable().cors();

        // 未登录认证异常
        http.exceptionHandling().authenticationEntryPoint(loginFailHandlerEntryPoint());

        // 登录过后访问无权限的接口时自定义403响应内容
        http.exceptionHandling().accessDeniedHandler(urlAccessDeniedHandler());

        // url权限认证处理
        registry.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
            @Override
            public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource());
                o.setAccessDecisionManager(urlAccessDecisionManager());
                return o;
            }
        });

        // OPTIONS(选项):查找适用于一个特定网址资源的通讯选择。 在不需执行具体的涉及数据传输的动作情况下, 允许客户端来确定与资源相关的选项以及 / 或者要求, 或是一个服务器的性能
        registry.antMatchers(HttpMethod.OPTIONS, "/**").denyAll();
        // 自动登录 - cookie储存方式
        registry.and().rememberMe();
        // 其余所有请求都需要认证
        registry.anyRequest().authenticated();
        // 防止iframe 造成跨域
        registry.and().headers().frameOptions().disable();

        // 自定义过滤器在登录时认证用户名、密码
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(securityFilter(), FilterSecurityInterceptor.class);

    }

    /**
     * 忽略拦截url或静态资源文件夹 - web.ignoring(): 会直接过滤该url - 将不会经过Spring Security过滤器链
     *                             http.permitAll(): 不会绕开springsecurity验证,相当于是允许该路径通过
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(HttpMethod.GET,
                "/favicon.ico",
                "/*.html",
                "/**/*.css",
                "/**/*.js");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    /**
     * 登录过滤器
     */
    @Bean
    public LoginFilter loginFilter(){
        return new LoginFilter();
    }

    /**
     * 登录失败处理类
     */
    @Bean
    public LoginFailHandlerEntryPoint loginFailHandlerEntryPoint(){
        return new LoginFailHandlerEntryPoint();
    };

    /**
     * 获取访问url所需要的角色信息
     */
    @Bean
    public UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource(){
        return new UrlFilterInvocationSecurityMetadataSource();
    };
    /**
     * 认证权限处理 - 将可以请求URL的角色权限与当前登录用户的角色做对比,如果包含其中一个角色即可正常访问
     */
    @Bean
    public UrlAccessDecisionManager urlAccessDecisionManager(){
        return new UrlAccessDecisionManager();
    };
    /**
     * 自定义访问无权限接口时403响应内容
     */
    @Bean
    public UrlAccessDeniedHandler urlAccessDeniedHandler(){
        return new UrlAccessDeniedHandler();
    };

    @Bean
    public SecurityFilter securityFilter() {
        return new SecurityFilter();
    }

    /**
     * 密码加密类
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new MD5PasswordEncoder();
    }

}

其他工具类

1、自定义异常
package com.example.security.url.exception;

import lombok.Data;

/**
 * @author Deyou Kong
 * @description 登录异常
 * @date 2023/2/13 9:18 上午
 */

@Data
public class LoginException extends RuntimeException{

    private String message;

    public LoginException(String message){
        this.message = message;
    }
}

2、读取配置文件配置
package com.example.security.url.property;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@Component
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsConfig {

    private List<String> urls;

}

3、MD5加密工具类
package com.example.security.url.utils;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * @author Deyou Kong
 * @description MD5算法
 * @date 2023/2/10 7:16 下午
 */

public class MD5Utils {
    /**
     * 使用md5的算法进行加密
     */
    public static String encode(String plainText) {
        byte[] secretBytes = null;
        try {
            secretBytes = MessageDigest.getInstance("md5").digest(
                    plainText.getBytes());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("没有md5这个算法!");
        }
        String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字
        // 如果生成数字未满32位,需要前面补0
        for (int i = 0; i < 32 - md5code.length(); i++) {
            md5code = "0" + md5code;
        }
        return md5code;
    }
}

4、MD5PasswordEncoder
package com.example.security.url.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author Deyou Kong
 * @description
 * @date 2023/2/10 7:16 下午
 */

@Slf4j
public class MD5PasswordEncoder implements PasswordEncoder {
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        log.info("MD5PasswordEncoder的matches");
        return encodedPassword.equals(MD5Utils.encode((String)rawPassword));
    }

    @Override
    public String encode(CharSequence rawPassword) {
        log.info("MD5PasswordEncoder的encode");
        return MD5Utils.encode((String)rawPassword);
    }
}

5、token工具类
package com.example.security.url.utils;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.example.security.url.constants.ResultConstant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.security.sasl.AuthenticationException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken生成的工具类
 * JWT token的格式:header.payload.signature
 * header的格式(算法、token的类型):
 * {"alg": "HS512","typ": "JWT"}
 * payload的格式(用户名、创建时间、生成时间):
 * {"sub":"wang","created":1489079981393,"exp":1489684781}
 * signature的生成算法:
 * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
 */

@Component
public class JwtTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";

    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    @Value("${jwt.tokenType}")
    private String tokenType;


    /**
     * 根据负责生成JWT的token
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)))
                .compact();
    }

    /**
     * 从token中获取JWT中的负载
     */
    private Claims getClaimsFromToken(String token) throws AuthenticationException {
        Claims claims = null;
        try {
            claims = Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException e){
            claims =e.getClaims();
        } catch (Exception e) {

            LOGGER.error("获取token:【{}】中的JWT负载失败:【{}】", token, e.getMessage());
        }
        return claims;
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token中获取登录用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 验证token中的用户是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) throws AuthenticationException {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     */
    public boolean isTokenExpired(String token) throws AuthenticationException {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) throws AuthenticationException {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据用户信息生成token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 当原来的token没过期时是可以刷新的
     *
     * @param oldToken 带tokenHead的token
     */
    public String refreshHeadToken(String oldToken) throws AuthenticationException {
        if (StrUtil.isEmpty(oldToken)) {
            return null;
        }
        String token = oldToken.substring(tokenType.length());
        if (StrUtil.isEmpty(token)) {
            return null;
        }
        //token校验不通过
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            return null;
        }
        //如果token已经过期,不支持刷新
        if (isTokenExpired(token)) {
            return null;
        }
        //如果token在30分钟之内刚刷新过,返回原token
        if (tokenRefreshJustBefore(token, 30 * 60)) {
            return token;
        } else {
            claims.put(CLAIM_KEY_CREATED, new Date());
            return generateToken(claims);
        }
    }

    /**
     * 判断token在指定时间内是否刚刚刷新过
     *
     * @param token 原token
     * @param time  指定时间(秒)
     */
    private boolean tokenRefreshJustBefore(String token, int time) throws AuthenticationException {
        Claims claims = getClaimsFromToken(token);
        Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
        Date refreshDate = new Date();
        //刷新时间在创建时间的指定时间内
        if (refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time))) {
            return true;
        }
        return false;
    }
}

6、输入流工具类
package com.example.security.url.utils;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.example.security.url.common.result.CommonResult;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author Deyou Kong
 * @description 响应处理类
 * @date 2023/2/9 3:33 下午
 */

public class ResponseUtils {

    public static void out(HttpServletResponse response, CommonResult result) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONObject.toJSONString(result, SerializerFeature.WriteMapNullValue)); // 保留值为null的字段
        response.getWriter().flush();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值