第十章🚦最全面的SpringSecurity+JWT 认证授权

578 阅读11分钟

🧑‍🎓 个人主页:花棉袄

📖 本章内容:【SpringSecurity+JWT 认证授权

image.png

🌼SpringBoot:搭建后端项目👉 Gitee仓库地址

一、SpringSecurity简介

  • SpringSecurity是一个用于Java 企业级应用程序的安全框架
  • 主要包含用户认证和用户授权两个方面
  • 相比较Shiro而言Security功能更加的强大
  • 它可以很容易地扩展以满足更多安全控制方面的需求
  • 但也相对它的学习成本会更高,两种框架各有利弊
  • 实际开发中还是要根据业务和项目的需求来决定使用哪一种
  • 快速入门:🚀Spring Security

二、JWT介绍

  • JWT是在Web应用中安全传递信息的规范,从本质上来说是Token的演变
  • 是一种生成加密用户身份信息的Token,特别适用于分布式单点登陆的场景
  • 无需在服务端保存用户的认证信息,而是直接对Token进行校验获取用户信息
  • 使单点登录更为简单灵活

三、身份验证架构

  • SecurityContextHolder:Spring Security 存储份验证者详细信息的位置

  • SecurityContext:从获取SecurityContextHolder并包含Authentication当前经过身份验证的用户的

  • Authentication:可以是输入以AuthenticationManager提供用户提供的用于身份验证的凭据或来自SecurityContext

  • GrantedAuthority:授予主体的权限Authentication(即角色、范围等)

  • AuthenticationManager:定义 Spring Security 的过滤器如何执行身份验证的API

  • ProviderManager:最常见的实现AuthenticationManager

  • AuthenticationProvider:用于ProviderManager执行特定类型的身份验证

  • Request Credentials withAuthenticationEntryPoint:用于从客户端请求凭据(即重定向到登录页面、发送WWW-Authenticate响应等)

  • AbstractAuthenticationProcessingFilter:Filter用于身份验证的基础。这也很好地了解了身份验证的高级流程以及各个部分如何协同工作

四、前期准备

graph LR


用户登录认证 --> session或者token保存会话-->判断用户角色-->判断权限-->获取对应资源

🕰️ 最小数据模型

image.png

🕰️ 数据库脚本

🌳sys_user 🌳sys_role 🌳sys_menu 🌳sys_role_menu

  • 👉 配置文件信息
# 用户配置
user:
  password:
    # 密码最大错误次数
    maxRetryCount: 5
    # 密码锁定时间(默认10分钟)
    lockTime: 10
    

# token配置
token:
  # 令牌自定义标识
  header: Authorization
  # 令牌密钥
  secret: K0TmVM#8O9u2end6V~QpYZ!!Xt
  # 令牌有效期(默认30分钟)
  expireTime: 30

🕰️ 创建国际化文件

  • 👉🏽 messages.properties**
#错误消息
not.null=* 必须填写
user.captcha.error=验证码错误
user.captcha.expire=验证码已失效
user.not.exists=用户不存在/密码错误
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
user.password.delete=对不起,您的账号已被删除
user.blocked=用户已封禁,请联系管理员
role.blocked=角色已封禁,请联系管理员
user.logout.success=退出成功
/**
 * Author: 花棉袄
 * Date: 2022年08月20日
 * Describe: 获取i18n资源文件
 */
public class MessageUtils {
    /**
     * 根据消息键和参数 获取消息 委托给spring messageSource
     *
     * @param code 消息键
     * @param args 参数
     * @return 获取国际化翻译值
     */
    public static String message(String code, Object... args) {
        MessageSource messageSource = SpringUtils.getBean(MessageSource.class);
        return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
    }
}

🕰️ 登录用户身份权限

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class LoginUserDetail implements UserDetails {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty("用户ID")
    private Long userId;

    @ApiModelProperty("部门ID")
    private Long deptId;

    @ApiModelProperty("用户唯一标识")
    private String token;

    @ApiModelProperty("登录时间")
    private Date loginTime;

    @ApiModelProperty("token的过期时间")
    private Long expireTime;

    @ApiModelProperty("登录IP地址")
    private String ipAddress;

    @ApiModelProperty("登录地点")
    private String loginLocation;

    @ApiModelProperty("浏览器类型")
    private String browser;

    @ApiModelProperty("操作系统")
    private String os;

    @ApiModelProperty("权限列表")
    private Set<String> permissions;

    @ApiModelProperty("用户信息")
    private SysUser sysUser;

    public LoginUserDetail(Long userId, Long deptId, SysUser sysUser, Set<String> permissions) {
        this.userId = userId;
        this.deptId = deptId;
        this.permissions = permissions;
        this.sysUser = sysUser;
    }

    public LoginUserDetail(Long userId, Set<String> permissions) {
        this.userId = userId;
        this.permissions = permissions;
    }

    /**
     * 授予用户权限
     *
     * @return 封装用户权限信息
     */
    @JSONField(serialize = false)
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @JSONField(serialize = false)
    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

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

    /**
     * 账户是否未过期,过期无法验证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     *
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     *
     * @return
     */
    @JSONField(serialize = false)
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 👉🏽登录信息:LoginBody
/**
 * @Author Michale
 * @CreateDate 2022/9/5
 * @Describe 登录信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginBody implements Serializable {

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("用户密码")
    private String password;


    @ApiModelProperty("验证码")
    private String code;

    @ApiModelProperty("唯一标识")
    private String uuid;
}
  • 👉 引入security依赖
<!-- ... other dependency elements ... -->
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-security</artifactId> 
</dependency>

🕰️ token工具服务类

public class TokenUtils {

    /**
     * 从数据生成令牌
     *
     * @param claims 数据
     * @return token
     */
    public String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
            .setClaims(claims)
            .signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret())
            .compact();
        return token;
    }


    /**
     * 从令牌中获取数据
     *
     * @param token 令牌
     * @return 数据声明
     */
    public Claims parseToken(String token) {
        Claims body = Jwts.parser()
            .setSigningKey(tokenProperties.getSecret())
            .parseClaimsJws(token)
            .getBody();
    return body;
    
    /**
     * 获取缓存的token键值
     *
     * @param uuid
     * @return
     */
    public String getTokenKey(String uuid) {
        return RedisCacheConstant.LOGIN_TOKEN_KEY + uuid;
    }
}

五、认证处理方法

image.png

🧭 Jwt认证过滤器

  • 首先用户请求进来直接拦截:从请求中获取token然后 ->从请求中获取用户身份信息
/**
 * @Author: 花棉袄
 * @CreateDate: 2022年08月30日
 * @Describe: token过滤器 验证token有效性
 */
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
    @Autowired
    private TokenUtils tokenUtils;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        LoginUserDetail loginUser = tokenUtils.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
            tokenUtils.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityUtils.setAuthentication(usernamePasswordAuthenticationToken);
        }
        filterChain.doFilter(request, response);
    }
}
  • 👉🏽TokenUtils工具类添加 刷新token有效期 的方法
/**
 * 验证令牌有效期,相差不足20分钟,自动刷新缓存
 *
 * @param loginUserDetail
 * @return 令牌
 */
public void verifyToken(LoginUserDetail loginUserDetail) {
    long expireTime = loginUserDetail.getExpireTime();
    long currentTime = System.currentTimeMillis();
    if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
        refreshToken(loginUserDetail);
    }
}

/**
 * 刷新令牌有效期
 *
 * @param loginUserDetail
 */
public void refreshToken(LoginUserDetail loginUserDetail) {
    loginUserDetail.setLoginTime(System.currentTimeMillis());
    loginUserDetail.setExpireTime(loginUserDetail.getLoginTime() + tokenProperties.getExpireTime() * MILLIS_MINUTE);
    // 根据uuid将loginUser缓存
    String userKey = getTokenKey(loginUserDetail.getToken());
    redisUtils.setCacheObject(userKey, loginUserDetail, tokenProperties.getExpireTime(), TimeUnit.MINUTES);
}
  • 👉🏽封装SecurityUtils工具类

image.png

  • 我们首先创建一个空的SecurityContext. 重要的是创建一个新SecurityContext实例
  • SecurityContextHolder.getContext().setAuthentication(authentication)它来避免跨多个线程的竞争条件
/**
 * 获取Authentication
 */
public static Authentication getAuthentication() {
    return SecurityContextHolder.getContext().getAuthentication();
}

/**
 * 设置Authentication
 *
 * @return
 */
public static void  setAuthentication(UsernamePasswordAuthenticationToken authentication) {
    SecurityContextHolder.getContext().setAuthentication(authentication);
}
  • 👉 TokenUtils添加相应的方法
    /**
     * 从请求中获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUserDetail getLoginUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token)) {
            try {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(CommonConstant.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginUserDetail user = redisUtils.getCacheObject(userKey);
                return user;
            } catch (Exception e) {
            }
        }
        return null;
    }

    /**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
     *
     * @param loginUserDetail
     * @return 令牌
     */
    public void verifyToken(LoginUserDetail loginUserDetail) {
        long expireTime = loginUserDetail.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
            refreshToken(loginUserDetail);
        }
    }
    
    /**
     * 刷新令牌有效期
     *
     * @param loginUserDetail
     */
    public void refreshToken(LoginUserDetail loginUserDetail) {
        loginUserDetail.setLoginTime(System.currentTimeMillis());
        loginUserDetail.setExpireTime(loginUserDetail.getLoginTime() + tokenProperties.getExpireTime() * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUserDetail.getToken());
        redisUtils.setCacheObject(userKey, loginUserDetail, tokenProperties.getExpireTime(), TimeUnit.MINUTES);
    }

🧭 认证失败处理类

/**
 * @Author: 花棉袄
 * @CreateDate: 2022年08月30日
 * @Describe: 认证失败处理类 返回未授权
 */
@Component
public class AuthenticationError implements AuthenticationEntryPoint, Serializable {
    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        int code = HttpStatus.UNAUTHORIZED.value();
        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}
  • 👉🏽将失败信息渲染到客户端
/**
 * 将字符串渲染到客户端
 *
 * @param response 渲染对象
 * @param string   待渲染的字符串
 */
public static void renderString(HttpServletResponse response, String string) {
    try {
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(string);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

🧭 自定义退出处理类

/**
 * @Author: 花棉袄
 * @CreateDate: 2022年08月30日
 * @Describe: 自定义退出处理类 返回成功
 */
@Configuration
public class LogoutSuccessHandler implements org.springframework.security.web.authentication.logout.LogoutSuccessHandler {
    @Autowired
    private TokenUtils tokenUtils;

    /**
     * 退出处理
     *
     * @return
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        LoginUserDetail loginUserDetail = tokenUtils.getLoginUser(request);
        if (StringUtils.isNotNull(loginUserDetail)) {
            String userName = loginUserDetail.getUsername();
            // 删除用户缓存记录
            tokenUtils.delLoginUser(loginUserDetail.getToken());
        }
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HTTPCodeMessage.SUCCESS.getCode(), "退出成功")));
    }
}
  • 👉 TokenUtils添加相应的方法
/**
 * 删除用户身份信息
 */
public void delLoginUser(String token) {
    if (StringUtils.isNotEmpty(token)) {
        String userKey = getTokenKey(token);
        redisUtils.deleteObject(userKey);
    }
}

六、授权处理

image.png

🍑 登录用户身份信息

@Slf4j
@Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) {
        //用户登录时:通过用户名字查询详细信息
        SysUser user = sysUserService.selectUserByUserName(username);
        if (StringUtils.isNull(user)) {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("用户验证处理", "登录用户:" + username + " 不存在");
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException("用户验证处理", "对不起,您的账号:" + username + " 已停用");
        }
        //自定义比对密码(通过用户名查询到的用户信息)
        passwordService.validate(user);
        //创建用户对象
        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUserDetail(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}
  • permissionService.getMenuPermission(user):从数据库中查询权限标识

🍑 封装用户权限信息

/**
 * @author 沉默的羔羊
 * 2022年9月03日
 * @apiNote 授权信息封装
 */
@Service("permissionService")
public class PermissionServiceImpl implements PermissionService {
    @Autowired
    private SysRoleService sysRoleService;

    @Autowired
    private SysMenuService sysMenuService;

    /**
     * 获取角色数据权限
     *
     * @param sysUser 用户信息
     * @return 角色数据权限 ("admin 或者 common ...")
     */
    @Override
    public Set<String> getRolePermission(SysUser sysUser) {
        Set<String> roles = new HashSet<String>();

        /*如果是管理员则拥有所有权限*/
        if (SecurityUtils.isAdmin(sysUser.getIdentity())) {
            roles.add("admin");
        } else {
            //查询普通用户的权限信息
            Set<String> permission = sysRoleService.selectPermissionByUserId(sysUser.getUserId());
            roles.addAll(permission);
        }
        return roles;
    }

    /**
     * 获取菜单数据权限
     *
     * @param sysUser 用户信息
     * @return 菜单数据权限 ("*.*.* 或者 system:user:list ...")
     */
    public Set<String> getMenuPermission(SysUser sysUser) {
        //封装菜单数据权限权限信息
        Set<String> menuPermission = new HashSet<String>();
        // 管理员拥有所有权限
        if (SecurityUtils.isAdmin(sysUser.getIdentity())) {
            menuPermission.add("*:*:*");
        } else {
            List<SysRole> roles = sysUser.getSysRoleList();
            if (!roles.isEmpty() &&roles.size() > 1) {
                for (SysRole sysRole : roles) {
                    Set<String> roleMenuPerms = sysMenuService.selectMenuPermsByRoleId(sysRole.getRoleId());
                    sysRole.setPermissions(roleMenuPerms);
                    menuPermission.addAll(roleMenuPerms);
                }
            } else {
                Set<String> roleMenuPerms = sysMenuService.selectMenuPermsByUserId(sysUser.getUserId());
                menuPermission.addAll(roleMenuPerms);
            }
        }
        return menuPermission;
    }
}

🍑 密码校验实现类

  • 👉🏽首先创建一个身份验证信息线程
- SecurityContextHolder使用 ThreadLocal来存储这些详细信息
- 这意味着SecurityContext始终可用于同一线程中的方法
- 即使SecurityContext未明确将其作为参数传递给这些方法
- ThreadLocal如果在处理当前主体的请求后注意清除线程
- 那么以这种方式使用是非常安全的
  • 👉Authentication包含
principal:识别用户。当使用用户名/密码进行身份验证时,这通常是UserDetails
credentials:通常是密码。在许多情况下,这将在用户通过身份验证后被清除,以确保它不被泄露
authorities:GrantedAuthoritys是授予用户的高级权限,一些示例是角色或范围
/**
 * @Author: 花棉袄
 * @CreateDate: 2022年08月30日
 * @Describe: 身份验证信息线程
 */
public class AuthenticationContext {
    private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();

    public static Authentication getContext() {
        return contextHolder.get();
    }

    public static void setContext(Authentication context) {
        contextHolder.set(context);
    }
    public static void clearContext() {
        contextHolder.remove();
    }

}
  • 👉🏽校验密码时:使用线程获取Authentication
//从上下文中获取Authentication 
Authentication usernamePasswordAuthenticationToken = AuthenticationContext.getContext();
//从Authentication中获取用户输入的信息 
String inUserName = usernamePasswordAuthenticationToken.getName(); 
String inPassWord = usernamePasswordAuthenticationToken.getCredentials().toString();
  • 👉密码校验实现类
@Slf4j
@Service("permissionService")
public class PasswordServiceImpl implements PasswordService {

    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    private PassWordProperties passWordProperties;


    /**
     * 登录密码方法
     *
     * @param user 通过用户名查询到的用户信息
     */
    @Override
    public void validate(SysUser user) {
        //从上下文中获取Authentication
        Authentication usernamePasswordAuthenticationToken = AuthenticationContext.getContext();

        //从Authentication中获取用户输入的信息
        String inUserName = usernamePasswordAuthenticationToken.getName();
        String inPassWord = usernamePasswordAuthenticationToken.getCredentials().toString();

        //查询密码错误输入次数
        Integer retryCount = getRetryCount(inUserName);
        if (retryCount == null) {
            retryCount = 0;
        }
        //判断次数是否大于密码最大错误次数
        if (retryCount >= passWordProperties.getMaxRetryCount()) {
            //抛出密码过长异常
            String message = MessageUtils.message("user.password.retry.limit.exceed");
            log.info(message);
            throw new UserPasswordMaxNumberExceptions(passWordProperties.getMaxRetryCount(), passWordProperties.getLockTime());
        }
        //进行密码比对
        if (!isCheckPassword(user, inPassWord)) {
            //比对失败记录输入次数
            String message = MessageUtils.message("user.password.retry.limit.count", retryCount);
            //将输入次数缓存到redis中
            cacheToRedis(inUserName, retryCount);
            throw new UserPasswordNotMatchException();
        } else {
            //登录成功之后清空记录
            clearLoginRecordCache(inUserName);
        }
    }
}
/**
 * 登录账户密码错误次数缓存键名
 *
 * @param userName 用户名
 * @return 缓存键key
 */
private String getCacheKey(String userName) {
    return RedisCacheConstant.PWD_ERR_CNT_KEY + userName;
}

/**
 * 查询密码错误输入次数
 *
 * @param inUserName 登录时输入的账户名
 * @return
 */
private Integer getRetryCount(String inUserName) {
    return redisUtils.getCacheObject(getCacheKey(inUserName));
}

/**
 * 将输入次数缓存到redis中
 *
 * @param userName   登录时输入的账户名
 * @param retryCount 记录错误次数
 */
private void cacheToRedis(String userName, Integer retryCount) {
    redisUtils.setCacheObject(getCacheKey(userName), retryCount, passWordProperties.getLockTime(), TimeUnit.MINUTES);
}

/**
 * 进行密码比对
 *
 * @param user       通过用户名查询到的账户信息
 * @param inPassWord 用户登录时输入的信息
 * @return
 */
private boolean isCheckPassword(SysUser user, String inPassWord) {
    boolean isCheckPassword = SecurityUtils.matchesPassword(inPassWord, user.getPassword());
    return isCheckPassword;
}

/**
 * 清空记录
 *
 * @param userName 登录时输入的账户名
 */
private void clearLoginRecordCache(String userName) {
    //清空redis中的缓存
    if (redisUtils.hasKey(getCacheKey(userName))) {
        redisUtils.deleteObject(getCacheKey(userName));
    }
}
  • 👉🏽封装SecurityUtils工具类
/**
 * 生成BCryptPasswordEncoder密码
 *
 * @param password 密码
 * @return 加密字符串
 */
public static String encryptPassword(String password) {
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    return passwordEncoder.encode(password);
}

/**
 * 判断密码是否相同
 *
 * @param rawPassword     真实密码
 * @param encodedPassword 加密后字符
 * @return 结果
 */
public static boolean matchesPassword(String rawPassword, String encodedPassword) {
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    return passwordEncoder.matches(rawPassword, encodedPassword);
}

七、核心配置类

🍎 匿名访问不鉴权注解

/**
 * @Author: 花棉袄
 * @CreateDate: 2022年08月30日
 * @Describe: 匿名访问不鉴权注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Anonymous {
}
  • 👉🏽 获取Anonymous注解的路径
/**
 * @Author: 花棉袄
 * @CreateDate: 2022年08月30日
 * @Describe: 获取Anonymous注解的路径
 */
@Component
public class Anonymous {
    /**
     * 获取标有注解 AnonymousAccess 的访问路径
     */
    public static String[] getAnonymousUrls() {
        // 获取所有的 RequestMapping
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = SpringUtils.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
        Set<String> allAnonymousAccess = new HashSet<>();
        // 循环 RequestMapping
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethods.entrySet()) {
            HandlerMethod value = infoEntry.getValue();
            // 获取方法上 Anonymous 类型的注解
            com.michale.framework.security.annotation.Anonymous methodAnnotation = value.getMethodAnnotation(com.michale.framework.security.annotation.Anonymous.class);
            // 如果方法上标注了 Anonymous 注解,就获取该方法的访问全路径
            if (methodAnnotation != null) {
                allAnonymousAccess.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            }
        }
        return allAnonymousAccess.toArray(new String[0]);
    }
}

🍎 配置类 SecurityConfig

  • 👉🏽SecurityConfig:配置类
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
  • 👉🏽解决 无法直接注入 AuthenticationManager

/**
 * 解决 无法直接注入 AuthenticationManager
 *
 * @return
 * @throws Exception
 */
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}
  • 👉🏽 强散列哈希加密实现
/**
 * 强散列哈希加密实现
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

/**
 * 身份认证接口
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
  • 👉🏽实现:configure
/**
 * anyRequest          |   匹配所有请求路径
 * access              |   SpringEl表达式结果为true时可以访问
 * anonymous           |   匿名可以访问
 * denyAll             |   用户不能访问
 * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
 * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
 * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
 * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
 * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
 * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
 * permitAll           |   用户可以任意访问
 * rememberMe          |   允许通过remember-me登录的用户访问
 * authenticated       |   用户登录后可访问
 */
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            // CSRF禁用,因为不使用session
            .csrf().disable()
            // 认证失败处理类
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
            // 基于token,所以不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            // 过滤请求
            .authorizeRequests()
            // 对于登录login 注册register 验证码captchaImage 允许匿名访问
            .antMatchers("/login", "/register", "/captchaImage").anonymous()
            // 静态资源,可匿名访问
            .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
            .antMatchers("/swagger-ui.html", "/swagger", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
            //允许匿名访问
            .antMatchers(Anonymous.getAnonymousUrls()).anonymous()
            // 除上面外的所有请求全部需要鉴权认证
            .anyRequest().authenticated()
            .and()
            .headers().frameOptions().disable();

    // 添加Logout filter
    httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
    // 添加JWT filter 在UsernamePasswordAuthenticationFilter之前
    httpSecurity.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
    // 添加CORS filter 在JwtAuthorizationFilter之前
    httpSecurity.addFilterBefore(corsFilter, JwtAuthorizationFilter.class);
    httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
  • 👉🏽配置加密规则
/**
 * 强散列哈希加密实现
 */
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder();
}

/**
 * 身份认证接口
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}

八、登录接口

🍏 用户登录实现

UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(username, password);
//将usernamePasswordAuthenticationToken放进线程中
AuthenticationContext.setContext(authenticationToken);
//将封装的Authenticate交给AuthenticationManager
authentication = authenticationManager.authenticate(authenticationToken);

👉🏽 校验验证码的方法

/**
 * 校验验证码
 *
 * @param code 验证码
 * @param uuid 唯一标识
 * @return 结果
 */
public void validateCaptcha(String code, String uuid) {
    String verifyKey = RedisCacheConstant.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
    String captcha = redisUtils.getCacheObject(verifyKey);
    redisUtils.deleteObject(verifyKey);
    if (captcha == null) {
        //验证码失效异常类
        throw new CaptchaExpireException();
    }
    if (!code.equalsIgnoreCase(captcha)) {
        //验证码错误异常类
        throw new CaptchaException();
    }
}
  • 👉🏽 鉴权成功之后就可以生成token
// 生成token
LoginUserDetail loginUserDetail = (LoginUserDetail) authentication.getPrincipal();
return tokenUtils.createToken(loginUserDetail);
/**
 * 创建令牌
 *
 * @param loginUserDetail 用户信息
 * @return 令牌
 */
public String createToken(LoginUserDetail loginUserDetail) {
    String token = IdUtils.fastUUID();
    loginUserDetail.setToken(token);
    setUserAgent(loginUserDetail);
    refreshToken(loginUserDetail);

    Map<String, Object> claims = new HashMap<>();
    claims.put(CommonConstant.LOGIN_USER_KEY, token);
    return createToken(claims);
}
/**
 * 设置用户代理信息
 *
 * @param loginUserDetail 登录信息
 */
public void setUserAgent(LoginUserDetail loginUserDetail) {
    UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
    String ip = IpUtils.getIpAddress(ServletUtils.getRequest());
    loginUserDetail.setIpaddr(ip);
    loginUserDetail.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
    loginUserDetail.setBrowser(userAgent.getBrowser().getName());
    loginUserDetail.setOs(userAgent.getOperatingSystem().getName());
}

🍏 登陆成功之后

  • 登陆成功之后可以从:Authentication获取用户信息
@GetMapping("/getInfo")
public AjaxResult getInfo() {
    SysUser user = SecurityUtils.getLoginUser().getUser();
    // 角色集合
    Set<String> roles = permissionService.getRolePermission(user);
    // 权限集合
    Set<String> permissions = permissionService.getMenuPermission(user);
    AjaxResult ajax = AjaxResult.success();
    ajax.put("user", user);
    ajax.put("roles", roles);
    ajax.put("permissions", permissions);
    return ajax;
}
  • 👉🏽 SecurityUtils添加获取用户信息的方法
/**
 * 获取用户
 **/
public static LoginUserDetail getLoginUser() {
    try {
        return (LoginUserDetail) getAuthentication().getPrincipal();
    } catch (Exception e) {
        throw new ServiceException("SecurityUtils_GetUserDetail", "获取用户信息异常", HTTPCodeMessage.UNAUTHORIZED.getCode());
    }
}

九、自定义权限注解

  • 👉🏽PermissionContextHolder权限信息
/**
 * @Author Michale
 * @CreateDate 2022/9/14
 * @Describe 权限信息
 */
public class PermissionContextHolder {
    private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT";

    public static void setContext(String permission) {
        RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission,
                RequestAttributes.SCOPE_REQUEST);
    }

    public static String getContext() {
        return ClassConvert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES,
                RequestAttributes.SCOPE_REQUEST));
    }
}

🍓 自定义权限校验

@Service("security")
public class MyPermissionServiceImpl implements MyPermissionService {

    /**
     * 所有权限标识
     */
    private static final String ALL_PERMISSION = "*:*:*";

    /**
     * 管理员角色权限标识
     */
    private static final String SUPER_ADMIN = "admin";

    private static final String ROLE_DELIMITER = ",";

    private static final String PERMISSION_DELIMITER = ",";
    /**
     * 判断是否包含权限
     *
     * @param permissions 权限列表
     * @param permission  权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermissions(Set<String> permissions, String permission) {
        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
    }
 }
  • 👉🏽验证用户是否具备某权限
/**
 * 验证用户是否具备某权限
 *
 * @param permission 权限字符串
 * @return 用户是否具备某权限
 */
@Override
public boolean isPermission(String permission) {
    if (StringUtils.isEmpty(permission)) {
        return false;
    }
    LoginUserDetail loginUser = SecurityUtils.getLoginUser();
    if (StringUtils.isNull(loginUser) || StringUtils.isEmpty(loginUser.getPermissions())) {
        return false;
    }
    PermissionContextHolder.setContext(permission);
    boolean isPermission = hasPermissions(loginUser.getPermissions(), permission);
    return isPermission;
}
  • 👉🏽验证用户是否不具备某权限
/**
 * 验证用户是否不具备某权限
 *
 * @param permission 权限字符串
 * @return 用户是否不具备某权限
 */
@Override
public boolean notPermission(String permission) {
    return isPermission(permission);

}
  • 👉🏽判断用户是否拥有某个角色
/**
 * 判断用户是否拥有某个角色
 *
 * @param permission 角色字符串
 * @return 用户是否拥有某个角色
 */
@Override
public boolean hasRole(String permission) {
    if (StringUtils.isEmpty(permission)) {
        return false;
    }
    LoginUserDetail loginUser = SecurityUtils.getLoginUser();
    if (StringUtils.isNull(loginUser) || StringUtils.isEmpty(loginUser.getUserVo().getRoles())) {
        return false;
    }
    for (RoleVo role : loginUser.getUserVo().getRoles()) {
        String roleKey = role.getRoleKey();
        if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(permission))) {
            return true;
        }
    }
    return false;
}
  • 👉🏽判断用户是否不拥有某个角色
/**
 * 判断用户是否不拥有某个角色
 *
 * @param permission 角色字符串
 * @return boolean
 */
@Override
public boolean notHasRole(String permission) {
    return hasRole(permission);

}

🍓 使用自定义的权限校验

@GetMapping("/hello")
@PreAuthorize("@security.hasRole('admin')")
@ApiOperation(value = "角色信息表查看详情", notes = "SysRole")
public AjaxResult getSysRoleById() {
    return AjaxResult.success().put("hello", "hello");
}
@GetMapping("/hello")
@PreAuthorize("@security.isPermission('system:user:query')")
@ApiOperation(value = "角色信息表查看详情", notes = "SysRole")
public AjaxResult getSysRoleById() {
    return AjaxResult.success().put("hello", "hello");
}
{
  "msg": "没有权限,要求用户的身份认证",
  "code": 401,
  "/system/hello": "不允许访问"
}

十、权限校验拦截器

@ApiModelProperty("权限校验异常")
@ExceptionHandler(AccessDeniedException.class)
public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    log.error("请求地址:'{}',权限校验失败:'{}'", requestURI, e.getMessage());
    return AjaxResult.error(UNAUTHORIZED.getCode(), UNAUTHORIZED.getMessage())
            .put(requestURI, e.getMessage());
}

🚦 报错问题解决

Error creating bean with name 'springSecurityFilterChain' ........
  • 🍒添加以下配置信息
spring:
    mvc:
      pathmatch:
        # 配置策略
        matching-strategy: ant-path-matcher