0


【万字长文】SpringBoot整合SpringSecurity+JWT+Redis完整教程(提供Gitee源码)

前言:最近在学习SpringSecurity的过程中,参考了很多网上的教程,同时也参考了一些目前主流的开源框架,于是结合自己的思路写了一个SpringBoot整合SpringSecurity+JWT+Redis完整的项目,从0到1写完感觉还是收获到不少的,于是我把我完整的笔记写成博客分享给大家,算是比较全的一个项目了,仅供大家参考和学习哦!

一、SpringSecurity简介

SpringSecurity是Spring生态系统中的安全管理框架,提供了一套Web应用安全性的完整解决方案。

它具有以下特点:

1、全面性:SpringSecurity提供了认证、授权、攻击防护等安全管理的全部功能。

2、扩展性:可以通过继承类、实现接口等方式轻松扩展SpringSecurity的功能。

3、与Spring无缝集成:可以与Spring框架完美整合,通过SpringIoC容器管理SpringSecurity组件。

4、防范常见攻击:可以防止脚本注入、会话固定、SQL注入等常见Web攻击。

5、配置简单:通过配置文件可以快速应用SpringSecurity带来的安全功能。

SpringSecurity的主要功能包括:

1、认证(Authentication):验证用户身份信息的合法性。

2、授权(Authorization):验证用户是否有权限执行操作。

3、防护攻击:防御如CSRF、Session固定、SQL注入等攻击。

4、方法安全:实现与系统方法的安全访问控制。

5、安全响应头:添加浏览器安全相关的响应头,提高安全性。

综上,SpringSecurity是一个MVC应用不可或缺的安全防护框架,为Java应用提供全面的安全支持。它与Spring框架集成紧密,配置简单,使用方便。

二、SpringSecurity认证流程

Spring Security认证流程中的几个核心类及其作用如下:

1、Authentication:认证信息接口,表示当前用户的认证信息,通常使用UsernamePasswordAuthenticationToken作为实现。

2、AuthenticationManager:认证管理器接口,authenticate()方法用来执行认证流程。

3、ProviderManager:认证管理器接口常用实现,封装多个AuthenticationProvider。

4、AuthenticationProvider:具体的认证处理器,由它完成特定的认证机制。

5、UserDetailsService:根据用户名加载用户信息,返回UserDetails接口的实现。

6、UserDetails:包含用户信息的接口,框架中代表用户信息。

7、UsernamePasswordAuthenticationFilter:处理表单登录认证的过滤器。

8、AbstractAuthenticationProcessingFilter:认证处理过滤器基类。

9、SecurityContextHolder:安全上下文容器,存取Authentication对象。

这是完整的SpringSecurity的认证流程:

1、用户向系统提交用户名和密码进行认证。

2、AuthenticationFilter会拦截请求,并从请求中提取出用户名和密码构造一个UsernamePasswordAuthenticationToken。

3、AuthenticationFilter将UsernamePasswordAuthenticationToken传入AuthenticationManager。

4、AuthenticationManager会找到一个匹配的AuthenticationProvider来进行认证。

5、AuthenticationProvider会先调用UserDetailsService的loadUserByUsername()方法根据用户名加载用户信息。

6、UserDetailsService根据用户名查询数据库,构造出一个UserDetails对象,包含用户信息、权限等。

7、AuthenticationProvider使用UserDetails和用户输入的密码进行匹配验证。如果匹配上就验证成功。

8、如果验证成功,AuthenticationProvider会构造一个已认证的Authentication对象。

9、AuthenticationProvider返回Authentication给AuthenticationManager。

10、AuthenticationManager将Authentication设置到SecurityContextHolder中。

11、后续的访问控制将使用SecurityContextHolder中的Authentication信息来验证用户身份和权限。

12、登录成功,用户访问系统受保护资源。

完整流程如图所示:

三、问题记录(重要)

阅读下面请务必先了解一下这个报错:

autoType is not support.org.springframework.security.core.authority.SimpleGrantedAuthority错误记录(亲测可用)

四、项目核心代码讲解

因为代码量比较庞大,所以我把整个项目的关键代码单独拿出来进行讲解,其他的次要的就不贴出来了,主要还是为了能够让大家更通俗易懂的去了解SpringSecurity的执行过程,完整的代码我会开源到Gitee,提供在文章的结尾。

4.1、导入pom依赖

完整的依赖都贴出来了。

<dependencies>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- lombok依赖包 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
            <scope>provided</scope>
        </dependency>

        <!-- spring security 安全认证 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- 单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <!-- redis依赖 对象池 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- pool 对象池 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.11.1</version>
        </dependency>

        <!-- 常用工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>

        <!-- 阿里JSON解析器 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.22</version>
        </dependency>

    </dependencies>

4.2、yml配置文件

主要配置了一个Redis连接以及Token常量。

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    password:
    timeout: 10s
    lettuce:
      pool:
        min-idle: 0
        max-idle: 8
        max-active: 8
        max-wait: -1ms

token:
  header: Authorization
  secret: oqwe9sdladwosqwqs
  expireTime: 30

4.3、实体类

一共涉及了四个实体类,主要设计了一些关键的字段,并不是非常完整。

4.3.1、LoginBody登录实体类

这个类主要用于接收前端传递过来的用户名和密码,然后去验证登录信息用的。

完整代码:

package com.example.security.domain;

import lombok.Data;

@Data
public class LoginBody
{
    /**
     * 用户名
     */
    private String username;

    /**
     * 用户密码
     */
    private String password;

}

4.3.2、Role角色类

存放每个用户的角色信息,需要实现序列化接口。

完整代码:

package com.example.security.domain;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

@Data
@AllArgsConstructor
public class Role implements Serializable {

  /**
   * 角色主键
   */
  private Long id;

  /**
   * 角色名称
   */
  private String name;

}

4.3.3、User用户类

主要存放的是用户的信息,需要实现序列化接口。

完整代码:

package com.example.security.domain;

import lombok.Data;

import java.io.Serializable;
import java.util.Set;

@Data
public class User implements Serializable {

    /**
     * 主键
     */
    private String id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 角色集合
     */
    private Set<Role> roles;

}

4.3.4、LoginUser登录用户信息

需要实现SpringSecurity自带的UserDetails接口,并实现它所有的方法,在Spring Security中,我们可以通过GrantedAuthority接口来表示一个用户所拥有的权限。
方法解释isAccountNonExpired()账号是否已过期isAccountNonLocked()账号是否已锁定isCredentialsNonExpired()凭(密码)是否已过期isEnabled()账号是否可用
这些方法返回true的目的是简化逻辑,在没有实现对应状态判断时,默认设置为true,这样可以避免不必要的认证/授权失败。

完整代码:

package com.example.security.domain;

import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Set;

@Data
public class LoginUser implements UserDetails {

    public LoginUser(User user,Set<GrantedAuthority> authorities)
    {
        this.user = user;
        this.authorities = authorities;

    }

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

    /**
     * 权限信息
     */
    private Set<GrantedAuthority> authorities;

    /**
     * token信息
     */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;
    /**
     * 过期时间
     */
    private Long expireTime;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.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 true;
    }
}

4.4、TokenService服务类

先注入一些参数。

关键代码:

@Value("${token.header}")
private String header;

@Value("${token.secret}")
private String secret;

@Value("${token.expireTime}")
private int expireTime;

protected static final long MILLIS_SECOND = 1000;

protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

4.4.1、生成令牌核心代码

关键代码:

    private String generateToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }

4.4.2、生成令牌关键逻辑

常量LOGIN_TOKEN_KEY:login_tokens:

1、首先生成一个随机的UUID作为token的值,设置到LoginUser对象中。

2、设置LoginUser的登录时间和过期时间(当前时间 + 过期时间)。

3、将LoginUser对象存储到Redis中,key为LOGIN_TOKEN_KEY+token,过期时间默认为yml配置的30分钟。

4、最终调用generateToken方法生成JWT token,传入claims哈希Map集合。

关键代码:

    public String createToken(LoginUser loginUser)
    {
        String token = UUID.randomUUID().toString();
        loginUser.setToken(token);
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        String userKey = CacheConstants.LOGIN_TOKEN_KEY + token;
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return generateToken(claims);
    }

4.4.3、解析令牌核心代码

关键代码:

 private Claims parseToken(String token)
    {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

4.4.4、获取请求头中携带的令牌

常量TOKEN_PREFIX:"Bearer "

1、从请求header中获取指定名称(yml文件配置的header)的authorization信息。

2、判断获得的token是否非空且以指定前缀(Constants.TOKEN_PREFIX)开头。

3、如果是,则移除前缀,得到最终的JWT token。

关键代码:

    private String getToken(HttpServletRequest request)
    {
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
        {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

4.4.5、获取Redis中存放的令牌Key

关键代码:

    private String getTokenKey(String uuid)
    {
        return CacheConstants.LOGIN_TOKEN_KEY + uuid;
    }

4.4.6、刷新令牌有效期

1、参数loginUser是当前登录的用户信息。

2、设置loginUser的新的登录时间为当前时间。

3、重新计算过期时间为当前时间+过期时间(yml文件配置的expireTime)。

4、根据登录用户的token作为key,存储更新后的loginUser到Redis中,并设置过期时间。

5、这样就相当于刷新了token的过期时间。

关键代码:

    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

4.4.7、验证令牌有效期

验证令牌有效期,相差不足20分钟,自动刷新缓存。

关键代码:

    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

4.4.8、获取用户身份信息

1、先从请求中获取JWT Token。

2、如果Token不为空,则对Token进行解析,获取claims。

3、从claims中取出对应的uuid。

4、根据uuid作为key,从Redis中获取LoginUser对象。

5、如果获取成功,返回LoginUser对象。

6、如果解析token或获取用户失败,则返回null。

关键代码:

    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            try
            {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginUser user = redisCache.getCacheObject(userKey);
                return user;
            }
            catch (Exception e)
            {
            }
        }
        return null;
    }

4.4.9、删除用户身份信息

通过token获取用户在redis中的唯一标识进行清除用户的登录数据,下次走过滤器的时候就会因没有此用户信息进行限制访问了!

    /**
     * 删除用户身份信息
     */
    public void delLoginUser(String token)
    {
        if (StringUtils.isNotEmpty(token))
        {
            String userKey = getTokenKey(token);
            redisCache.deleteObject(userKey);
        }
    }

4.5、AuthenticationEntryPointImpl配置认证失败处理类

常量UNAUTHORIZED:401

AuthenticationEntryPoint是SpringSecurity中用于处理认证失败的接口,用于未登录或登录过期的情况,会触发commence方法。

1、在方法内部,首先设置了响应状态码为401 Unauthorized。

2、然后使用StringUtils生成了一个错误信息字符串,包含请求访问的接口路径和认证失败的提示。3、最后使用AjaxResult把状态码和错误信息封装成一个结果,通过ServletUtils以JSON格式写入响应中。

4、AjaxResult是一个封装AJAX请求结果的类,可以方便地生成错误或成功的响应结果。

5、ServletUtils是一个工具类,可以方便地将String数据渲染到HttpServletResponse中。

所以这个类的作用就是在认证失败时,以JSON格式返回一个包含错误代码和消息的结果到前端,前端可以根据这个结果显示对应提示或做处理。

关键代码:

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint
{
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}

4.6、JwtAuthenticationTokenFilter认证过滤器

OncePerRequestFilter是SpringSecurity提供的一个过滤器基类,主要用于保证过滤器在一个请求内只执行一次,JwtAuthenticationTokenFilter需要继承这个基类并重写doFilterInternal的方法。

1、通过tokenService从请求头中提取JWT token,并解析得到LoginUser对象

2、调用tokenService的verifyToken方法验证JWT token的有效性

3、使用LoginUser对象构建一个UsernamePasswordAuthenticationToken

4、设置AuthenticationToken的细节,如请求来源等

5、将构造好的UsernamePasswordAuthenticationToken对象设置到SecurityContextHolder的Context中。

6、这样登录用户的Authentication对象就保存到了安全上下文中。

7、最后过滤器链继续向后执行doFilter方法。

这样在过滤器中就实现了对token的解析和验证,并设置了Authentication对象到安全上下文中,
后续的过滤器就可以依据它来判断用户认证信息了。

关键代码:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{

    @Value("${token.header}")
    private String header;

    @Value("${token.secret}")
    private String tokenKey;

    @Resource
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {

        LoginUser loginUser = tokenService.getLoginUser(request);
        if (loginUser != null)
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

** 注:如果你要获取当前登录的用户信息,可以通过以下2行代码进行获取。**

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();

4.7、FastJson序列化

package com.example.security.config;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    public FastJson2JsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
    }
}

4.8、自定义Redis序列化

注:详细讲解在这篇文章:SpringBoot整合RedisTemplate操作Redis数据库详解(提供Gitee源码)

package com.example.security.config;

import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置
 */
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
        GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

}

4.9、Redis工具类

package com.example.redis.utils;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
 
import java.util.*;
import java.util.concurrent.TimeUnit;
 
/**
 * spring redis 工具类
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;
 
    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }
 
    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }
 
    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }
 
    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }
 
    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public long getExpire(final String key)
    {
        return redisTemplate.getExpire(key);
    }
 
    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key)
    {
        return redisTemplate.hasKey(key);
    }
 
    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }
 
    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }
 
    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public boolean deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection) > 0;
    }
 
    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }
 
    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }
 
    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }
 
    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }
 
    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }
 
    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }
 
    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }
 
    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }
 
    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }
 
    /**
     * 删除Hash中的某条数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey)
    {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }
 
    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
 
    /**
     * 存储有序集合
     * @param key 键
     * @param value 值
     * @param score 排序
     */
    public void zSet(Object key, Object value, double score){
        redisTemplate.opsForZSet().add(key, value, score);
    }
 
    /**
     * 存储值
     * @param key 键
     * @param set 集合
     */
    public void zSet(Object key, Set set){
        redisTemplate.opsForZSet().add(key, set);
    }
 
    /**
     * 获取key指定范围的值
     * @param key 键
     * @param start 开始位置
     * @param end 结束位置
     * @return 返回set
     */
    public Set zGet(Object key, long start, long end){
        Set set = redisTemplate.opsForZSet().range(key, start, end);
        return set;
    }
 
    /**
     * 获取key对应的所有值
     * @param key 键
     * @return 返回set
     */
    public Set zGet(Object key){
        Set set = redisTemplate.opsForZSet().range(key, 0, -1);
        return set;
    }
 
    /**
     * 获取对用数据的大小
     * @param key 键
     * @return 键值大小
     */
    public long zGetSize(Object key){
        Long size = redisTemplate.opsForZSet().size(key);
        return size;
    }
}

4.10、SecurityConfig核心配置类

这是核心代码,注释都在代码上面了,可以自己仔细看一下,这边就不多做阐述。

完整代码:

package com.example.security.config;

import com.example.security.filter.JwtAuthenticationTokenFilter;
import com.example.security.service.serviceImpl.AuthenticationEntryPointImpl;
import com.example.security.service.serviceImpl.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;

import javax.annotation.Resource;

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;

    /**
     * token认证过滤器
     */
    @Resource
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 认证失败处理类
     */
    @Resource
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Resource
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    @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()
                //允许登录接口匿名访问
                .antMatchers("/login").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加Logout filter
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

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

4.11、AuthenticationContextHolder线程本地的存储

主要提供了以下几个静态方法:

1、getContext()获取当前线程的Authentication对象。

2、setContext(Authentication context)设置当前线程的Authentication对象。

3、clearContext()清除当前线程的Authentication对象。

它使用ThreadLocal维护线程隔离,所以每个线程拥有自己的Authentication信息,互不干扰。

在Spring Security中,可以通过该类在不同层传递认证信息。

关键代码:

public class AuthenticationContextHolder
{
    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();
    }
}

4.12、UserServiceImpl查询用户接口

这边我偷懒了没有连接数据库,这个密码是通过如下代码加密获得的。

关键代码:

PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
System.out.println(encode);

主要是为了演示,所以这边直接把用户信息和角色信息写死了,实际开发还是需要连接数据库的。

关键代码:

@Service
public class UserServiceImpl {

    public User selectUserByUsername(String username){
        User user = new User();
        user.setId(UUID.randomUUID().toString());
        user.setUsername(username);
        user.setPassword("$2a$10$ErrO7WgkEBAWVQwuJtbBve7R2.pSKUrfs7zt8XkASqJKqcetMvAUC");
        Set<Role> roles = new HashSet<>();
        Role role1 = new Role(1L, "ROLE_ADMIN");
        Role role2 = new Role(2L, "ROLE_USER");
        roles.add(role1);
        roles.add(role2);
        user.setRoles(roles);
        return user;
    }
}

这边用户必须以ROLE_作为前缀,具体分析看3.15的@PreAuthorize注解分析

4.13、PasswordServiceImpl密码验证服务类

1、validate()方法用来验证用户密码。

2、它先从AuthenticationContextHolder中获取当前认证的用户名和密码。

3、然后调用matches()方法来校验密码。

4、matches()方法使用BCryptPasswordEncoder对存储的密文密码进行匹配验证。

5、如果匹配成功则验证成功,失败则验证失败。

这样通过Spring Security的AuthenticationContextHolder可以获取到当前认证principal的信息。

再结合密码加密匹配验证,就可以在服务中方便的实现密码的验证。

关键代码:

@Service
public class PasswordServiceImpl {

    public void validate(User user)
    {
        Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
        String AuthUsername = usernamePasswordAuthenticationToken.getName();
        String AuthPassword = usernamePasswordAuthenticationToken.getCredentials().toString();

        if (matches(user, AuthPassword)) {
            System.out.println("验证成功!");
        } else {
            System.out.println("验证失败!");
        }
    }

    public boolean matches(User user, String rawPassword)
    {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, user.getPassword());
    }
}

4.14、UserDetailsServiceImpl认证用户服务类

1、实现Spring Security的UserDetailsService接口。

UserDetailsService是Spring Security用于加载用户信息的核心接口。自定义实现可以灵活控制用户信息加载过程。

2、根据用户名加载用户信息通过userService查询数据库获取用户对象,包含用户信息如用户名、密码、角色等。

3、验证用户密码使用passwordService进行密码验证,校验登录的密码是否正确。

4、构建用户权限信息将用户的角色信息转换成GrantedAuthority授权信息集合。

5、封装用户对象返回将用户信息、权限信息封装到LoginUser对象中返回作为UserDetails。

6、在登录验证时提供用户详细信息Spring Security在登录验证时会调用此服务获取用户详细信息,以进行认证和授权。

关键代码:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private PasswordServiceImpl passwordService;

    @Resource
    private UserServiceImpl userService;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userService.selectUserByUsername(username);
        passwordService.validate(user);
        //取出角色和权限信息
        Set<Role> roles = user.getRoles();

        Set<GrantedAuthority> authorities = new HashSet<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return new LoginUser(user,authorities);
    }

}

4.15、LoginController登录接口

所有的登录都必须走这个接口,登录成功以后会返回给用户一个令牌,以访问系统受保护的资源。

关键代码:

@RestController
public class LoginController {

    @Resource
    private LoginServiceImpl loginService;

    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

}

4.16、LoginServiceImpl登录接口核心逻辑

1、创建UsernamePasswordAuthenticationToken,包含用户名和密码。

2、将该authenticationToken设置到AuthenticationContextHolder,这是Spring Security提供的一个存储authentication的holder。

3、调用AuthenticationManager的authenticate方法进行认证。这个方法会根据配置调用相关的UserDetailsService等进行认证。(下面会走认证用户信息服务类中的loadUserByUsername方法),验证成功以后会返回一个authentication对象。

4、清空AuthenticationContextHolder。

5、从authentication对象中获取登录用户信息LoginUser。

6、使用TokenService生成JWT token。

7、返回JWT token。

关键代码:

@Service
public class LoginServiceImpl {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private TokenService tokenService;

    public String login(String username, String password)
    {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        AuthenticationContextHolder.setContext(authenticationToken);
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        AuthenticationContextHolder.clearContext();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        return tokenService.createToken(loginUser);
    }

}

4.17、LogoutSuccessHandlerImpl退出登录核心逻辑

LogoutSuccessHandlerImpl实现了Spring Security的LogoutSuccessHandler接口,用于处理用户退出登录成功后的逻辑。

1、通过TokenService获取当前登录的用户信息LoginUser。

2、如果LoginUser不为空,则调用TokenService的delLoginUser方法删除该用户的缓存信息。这里涉及到一个自定义的TokenService,用于处理基于令牌的用户认证。登录时生成一个令牌与用户信息对应,退出时需要删除这个缓存关系。

3、使用ServletUtils向响应写入退出成功的提示,这样ajax请求可以获取到提示信息。

关键代码:

/**
 * 退出登录
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
    @Autowired
    private TokenService tokenService;

    /**
     * 退出处理
     * @return
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser))
        {
            // 删除用户缓存记录
            tokenService.delLoginUser(loginUser.getToken());
        }
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
    }
}

4.18、@PreAuthorize注解

这个注解是打在接口上的,用于标记该接口只有指定的用户才有权限访问。

1、在接口上使用**@PreAuthorize("hasRole('USER')")**这个注解时候,内部源码执行的流程我可以给大家剖析一下。

首先获取上面我们配置好的接口参数为USER

进入hasAnyAuthorityName方法,获取当前用户的所有权限(roleSet集合),然后用for循环去遍历注解上的参数数组,这边只循环了一次,因为注解上只配了一个USER,所以数组长度为1,如果当前用户的所有权限集合中包含当前接口的权限信息则放行,否则不放行。

3、getRoleWithDefaultPrefix方法是用来检测当前传递进来的role是否以默认的**ROLE_为前缀,如果有则直接返回,没有的话再拼上ROLE_**的默认前缀进行返回。

这就是这个注解的验证过程,以此也解释了3.9中为什么用户权限必须以**ROLE_**为前缀打头的原因。

4.19、HelloController测试接口

分别写个2个测试接口,一个是登陆后谁都可以访问的接口,一个是登陆后需要USER权限的用户才可以访问的接口,还有一个是登录后需要COMMON权限才可以访问的接口。

关键代码:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "Hello World!";
    }

    @GetMapping("/user")
    @PreAuthorize("hasRole('USER')")
    public String user(){
        return "Hello USER!";
    }

    @GetMapping("/common")
    @PreAuthorize("hasRole('COMMON')")
    public String common(){
        return "Hello COMMON!";
    }
}

注意:必须用public方法修饰,否则@PreAuthorize注解失效!

4.20、总结

最后我们通过实际的项目来总结一下本次SpringSecurity的认证流程:

1、在LoginService的login方法中,构造一个UsernamePasswordAuthenticationToken,包含用户名和密码,这是开始认证的入口。

2、LoginService调用AuthenticationManager的authenticate方法启动认证流程。

3、AuthenticationManager会找到一个匹配的AuthenticationProvider来进行认证。

4、AuthenticationProvider会调用UserDetailsService的loadUserByUsername方法加载用户信息。这里我们通过UserDetailsServiceImpl来查询用户。

5、在UserDetailsServiceImpl中,根据用户名查询用户信息,然后调用PasswordService进行密码验证。

6、PasswordService通过AuthenticationContextHolder获取登录的用户名和密码。然后与数据库中存储的用户密码(经过编码)进行匹配,如果匹配上就验证成功。

7、PasswordService验证成功后,UserDetailsServiceImpl将根据用户信息构造一个UserDetails对象(这里是LoginUser),包含了用户名,密码,权限信息等。

8、UserDetailsServiceImpl将UserDetails返回给AuthenticationProvider。

9、AuthenticationProvider收到UserDetails后,完成验证,并生成一个已认证的Authentication对象。

10、AuthenticationProvider将Authentication返回给AuthenticationManager。

11、AuthenticationManager设置该Authentication到SecurityContextHolder中,供后续访问控制使用。

12、LoginService拿到已认证的Authentication,从中取出UserDetails,生成JWTtoken并返回。

综上,结合项目的逻辑SpringSecurity的认证流程大体可以分为:获取用户信息->用户验证->构建UserDetails->生成Authentication。我们通过自定义UserDetailsService和PasswordService来实现了用户验证逻辑。

五、运行项目

5.1、登录成功

通过post请求发送json格式的数据进行登录。

登录成功了并返回了Token令牌!

5.2、访问无权限接口

把刚才获得的令牌设置到请求头当中,进行访问测试接口。

5.3、访问需要USER权限的接口

把hello改成user。

因为这个用户默认配置了ADMIN和USER两个权限,所以可以访问成功!

5.4、访问需要COMMON权限的接口

把user改成common。

很明显,返回了403的错误信息。

5.5、退出登录

将common改成logout。

退出登录成功。

5.6、访问失败

上面退出登录了,看看还能不能通过携带之前的token访问测试接口。

可以很明显的看到,系统返回了401的错误。

5.7、登录失败

上面我们都是登录成功的例子,我们这次故意输错密码,看看能不能登录成功。

可以很明显的看到,系统返回了401的错误。

六、Gitee源码地址

因为本篇博客提供的代码不是完整的,所以我把完整的项目开源到了码云上,供大家学习和参考!

项目地址:SpringBoot整合SpringSecurity+JWT+Redis完整教程

七、总结

以上就是我对于SpringSecurity以及如何在实际项目当中开发应用的个人理解,如有问题欢迎评论区留言!


本文转载自: https://blog.csdn.net/HJW_233/article/details/131969622
版权归原作者 黄团团 所有, 如有侵权,请联系我们删除。

“【万字长文】SpringBoot整合SpringSecurity+JWT+Redis完整教程(提供Gitee源码)”的评论:

还没有评论