添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
重情义的冲锋衣  ·  java - How to resolve ...·  1 年前    · 
坚强的打火机  ·  Ajax 表编辑器和查看器·  1 年前    · 
  • 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保存会话-->判断用户角色-->判断权限-->获取对应资源
    

    🕰️ 最小数据模型

    🕰️ 数据库脚本

    🌳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=退出成功
    
  • 👉 spring工具类 方便在非spring管理环境中获取bean
  • 👉 获取i18n资源文件
  • * 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;
    

    五、认证处理方法

    🧭 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工具类
  • 我们首先创建一个空的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);

    六、授权处理

    🍑 登录用户身份信息

    @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:
          pathmatch:
            # 配置策略
            matching-strategy: ant-path-matcher
    复制代码
    分类:
    后端
  •