SpringSecurity+JWT快速入门

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: SpringSecurity+JWT快速入门

@[TOC]

1 简介

1.1 认证和授权

认证和授权是SpringSecurity作为安全框架的核心功能

  • 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
  • 授权:经过认证后判断当前用户是否有权限进行某个操作
  • 校验:再次访问时,判断是否有token

    1.2 spring security的核心过滤器链

  • UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后的登录请求。
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedExceptionAuthenticationException
  • FilterSecurityInterceptor:负责权限校验的过滤器。

    1.3 入门案例认证流程

  • 1、UsernamePasswordAuthenticationFilter将用户名和密码封装于Authentication,此时,对象内只有用户名和密码,还没有权限信息
  • 2、调用authenticate方法进行认证,传到ProviderManager
  • 3、再调用DaoAuthenticationProviderauthenticate方法进行认证
  • 4、再调用InMemoryUserDetailsManagerloadUserByUsername方法去内存中查询用户及其权限,并将用户信息和权限封装成UserDetails对象返回到DaoAuthenticationProvider
  • 5、通过PasswordEncoder对比UserDetailsAuthentication的密码是否正确,如果正确就把权限信息设置到Authentication返回到UsernamePasswordAuthenticationFilter
  • 6、如果在UsernamePasswordAuthenticationFilter返回了Authentication对象就使用SecurityContextHolder.getContext().setAuthentication()方法储存该对象。其他过滤器可以通过SecurityContextHolder来获取当前用户信息。

    1.4 自定义SpringSecurity认证、授权、校验

    1.4.1 登录

  • 自定义登录接口:调用ProviderManager的方法进行认证如果认证通过生成jwt,并把用户信息存入redis
  • 自定义UserDetailsService,在这个实现类中去查询数据库用户信息

    1.4.2 校验

    定义jwt认证过滤器
  • 获取token
  • 解析token获取其中的userid
  • 从redis中获取用户信息
  • 存入SecurityContextHolder

    2 快速入门

    2.1 pom引入依赖

    <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter</artifactId>
          </dependency>
    
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-test</artifactId>
              <scope>test</scope>
          </dependency>
    
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-web</artifactId>
          </dependency>
    
          <!-- SpringSecurity启动器 -->
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-security</artifactId>
          </dependency>
    
          <!-- redis依賴 -->
          <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-data-redis</artifactId>
          </dependency>
    
          <!-- fastjson依赖 -->
          <dependency>
              <groupId>com.alibaba.fastjson2</groupId>
              <artifactId>fastjson2</artifactId>
              <version>2.0.20</version>
          </dependency>
    
          <!-- jwt依赖 -->
          <dependency>
              <groupId>io.jsonwebtoken</groupId>
              <artifactId>jjwt</artifactId>
              <version>0.9.0</version>
          </dependency>
    
          <!-- mybatis-plus -->
          <dependency>
              <groupId>com.baomidou</groupId>
              <artifactId>mybatis-plus-boot-starter</artifactId>
              <version>3.5.1</version>
          </dependency>
    
          <!-- mysql驱动 -->
          <dependency>
              <groupId>mysql</groupId>
              <artifactId>mysql-connector-java</artifactId>
              <scope>runtime</scope>
          </dependency>
    
          <dependency>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
              <optional>true</optional>
          </dependency>
    

    2.2 创建domain包

    2.2.1 新建AjaxResult

    ```java
    package com.yunfeng.tokendemo.domain;

import com.yunfeng.tokendemo.common.HttpStatus;
import org.springframework.util.ObjectUtils;

import java.util.HashMap;

/**

  • 操作消息提醒
    */
    public class AjaxResult extends HashMap
    {
    private static final long serialVersionUID = 1L;

    /* 状态码 /
    public static final String CODE_TAG = "code";

    /* 返回内容 /
    public static final String MSG_TAG = "msg";

    /* 数据对象 /
    public static final String DATA_TAG = "data";

    /**

    • 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。
      */
      public AjaxResult()
      {
      }

      /**

    • 初始化一个新创建的 AjaxResult 对象
      *
    • @param code 状态码
    • @param msg 返回内容
      */
      public AjaxResult(int code, String msg)
      {
      super.put(CODE_TAG, code);
      super.put(MSG_TAG, msg);
      }

      /**

    • 初始化一个新创建的 AjaxResult 对象
      *
    • @param code 状态码
    • @param msg 返回内容
    • @param data 数据对象
      */
      public AjaxResult(int code, String msg, Object data)
      {
      super.put(CODE_TAG, code);
      super.put(MSG_TAG, msg);
      if (ObjectUtils.isEmpty(data)) {

       super.put(DATA_TAG, data);
      

      }
      }

      /**

    • 返回成功消息
      *
    • @return 成功消息
      */
      public static AjaxResult success()
      {
      return AjaxResult.success("操作成功");
      }

      /**

    • 返回成功数据
      *
    • @return 成功消息
      */
      public static AjaxResult success(Object data)
      {
      return AjaxResult.success("操作成功", data);
      }

      /**

    • 返回成功消息
      *
    • @param msg 返回内容
    • @return 成功消息
      */
      public static AjaxResult success(String msg)
      {
      return AjaxResult.success(msg, null);
      }

      /**

    • 返回成功消息
      *
    • @param msg 返回内容
    • @param data 数据对象
    • @return 成功消息
      */
      public static AjaxResult success(String msg, Object data)
      {
      return new AjaxResult(HttpStatus.SUCCESS, msg, data);
      }

      /**

    • 返回警告消息
      *
    • @param msg 返回内容
    • @return 警告消息
      */
      public static AjaxResult warn(String msg)
      {
      return AjaxResult.warn(msg, null);
      }

      /**

    • 返回警告消息
      *
    • @param msg 返回内容
    • @param data 数据对象
    • @return 警告消息
      */
      public static AjaxResult warn(String msg, Object data)
      {
      return new AjaxResult(HttpStatus.WARN, msg, data);
      }

      /**

    • 返回错误消息
      *
    • @return 错误消息
      */
      public static AjaxResult error()
      {
      return AjaxResult.error("操作失败");
      }

      /**

    • 返回错误消息
      *
    • @param msg 返回内容
    • @return 错误消息
      */
      public static AjaxResult error(String msg)
      {
      return AjaxResult.error(msg, null);
      }

      /**

    • 返回错误消息
      *
    • @param msg 返回内容
    • @param data 数据对象
    • @return 错误消息
      */
      public static AjaxResult error(String msg, Object data)
      {
      return new AjaxResult(HttpStatus.ERROR, msg, data);
      }

      /**

    • 返回错误消息
      *
    • @param code 状态码
    • @param msg 返回内容
    • @return 错误消息
      */
      public static AjaxResult error(int code, String msg)
      {
      return new AjaxResult(code, msg, null);
      }

      /**

    • 方便链式调用
      *
    • @param key 键
    • @param value 值
    • @return 数据对象
      */
      @Override
      public AjaxResult put(String key, Object value)
      {
      super.put(key, value);
      return this;
      }
      }
      ```

      2.2.2 新建User类

      ```java
      package com.yunfeng.tokendemo.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.io.Serializable;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User implements Serializable {

private static final long serialVersionUID = -403567854238683121L;

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

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

/**
 * 昵称
 */
private String nickName;

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

/**
 * 账号状态(0正常 1停用)
 */
private String status;

/**
 * 邮箱
 */
private String email;

/**
 * 手机号
 */
private String phonenumber;

/**
 * 用户性别(0男,1女,2未知)
 */
private String sex;

/**
 * 头像
 */
private String avatar;

/**
 * 用户类型(0管理员,1普通用户)
 */
private String userType;

/**
 * 创建人的用户id
 */
private Long createBy;

/**
 * 创建时间
 */
private Date createTime;

/**
 * 更新人
 */
private Long updateBy;

/**
 * 更新时间
 */
private Date updateTime;

/**
 * 删除标志(0代表未删除1代表已删除)
 */
private Integer delFlag;

}

## 2.3 创建common包
### 2.3.1 创建HttpStatus类
```java
package com.yunfeng.tokendemo.common;

/**
 * 返回状态码
 */
public class HttpStatus
{
    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    /**
     * 对象创建成功
     */
    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    /**
     * 操作已经执行成功,但是没有返回数据
     */
    public static final int NO_CONTENT = 204;

    /**
     * 资源已被移除
     */
    public static final int MOVED_PERM = 301;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 资源没有被修改
     */
    public static final int NOT_MODIFIED = 304;

    /**
     * 参数列表错误(缺少,格式不匹配)
     */
    public static final int BAD_REQUEST = 400;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限,授权过期
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    /**
     * 不允许的http方法
     */
    public static final int BAD_METHOD = 405;

    /**
     * 资源冲突,或者资源被锁
     */
    public static final int CONFLICT = 409;

    /**
     * 不支持的数据,媒体类型
     */
    public static final int UNSUPPORTED_TYPE = 415;

    /**
     * 系统内部错误
     */
    public static final int ERROR = 500;

    /**
     * 接口未实现
     */
    public static final int NOT_IMPLEMENTED = 501;

    /**
     * 系统警告消息
     */
    public static final int WARN = 601;
}

2.4 创建config包

2.4.1 创建RedisConfig类

package com.yunfeng.tokendemo.config;

import com.yunfeng.tokendemo.utils.FastJson2JsonRedisSerializer;
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.core.script.DefaultRedisScript;
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 connectionFactory)
    {
   
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

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

        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public DefaultRedisScript<Long> limitScript()
    {
   
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(limitScriptText());
        redisScript.setResultType(Long.class);
        return redisScript;
    }

    /**
     * 限流脚本
     */
    private String limitScriptText()
    {
   
        return "local key = KEYS[1]\n" +
                "local count = tonumber(ARGV[1])\n" +
                "local time = tonumber(ARGV[2])\n" +
                "local current = redis.call('get', key);\n" +
                "if current and tonumber(current) > count then\n" +
                "    return tonumber(current);\n" +
                "end\n" +
                "current = redis.call('incr', key)\n" +
                "if tonumber(current) == 1 then\n" +
                "    redis.call('expire', key, time)\n" +
                "end\n" +
                "return tonumber(current);";
    }
}

2.5 创建utils包

2.5.1 创建FastJson2JsonRedisSerializer包

package com.yunfeng.tokendemo.utils;

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

/**
 * 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);
    }
}

2.5.2 创建JwtUtils类

package com.yunfeng.tokendemo.utils;

import java.util.Map;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

/**
 * Jwt工具类
 */
public class JwtUtils
{
   
    public static Long JWT_TTL = 60*60*1000l; //一个小时

    /**
     * 令牌秘钥
     */
    public final static String secret = "abcdefghijklmnopqrstuvwxyz";

    /**
     * 用户ID字段
     */
    public static final String DETAILS_USER_ID = "user_id";

    /**
     * 登录用户
     */
    public static final String LOGIN_USER = "login_user";

    /**
     * 用户标识
     */
    public static final String USER_KEY = "user_key";

    /**
     * 用户名字段
     */
    public static final String DETAILS_USERNAME = "username";

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

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    public static Claims parseToken(String token)
    {
   
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    /**
     * 根据令牌获取用户标识
     *
     * @param token 令牌
     * @return 用户ID
     */
    public static String getUserKey(String token)
    {
   
        Claims claims = parseToken(token);
        return getValue(claims, USER_KEY);
    }

    /**
     * 根据令牌获取用户标识
     *
     * @param claims 身份信息
     * @return 用户ID
     */
    public static String getUserKey(Claims claims)
    {
   
        return getValue(claims, USER_KEY);
    }

    /**
     * 根据令牌获取用户ID
     *
     * @param token 令牌
     * @return 用户ID
     */
    public static String getUserId(String token)
    {
   
        Claims claims = parseToken(token);
        return getValue(claims, DETAILS_USER_ID);
    }

    /**
     * 根据身份信息获取用户ID
     *
     * @param claims 身份信息
     * @return 用户ID
     */
    public static String getUserId(Claims claims)
    {
   
        return getValue(claims, DETAILS_USER_ID);
    }

    /**
     * 根据令牌获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public static String getUserName(String token)
    {
   
        Claims claims = parseToken(token);
        return getValue(claims, DETAILS_USERNAME);
    }

    /**
     * 根据身份信息获取用户名
     *
     * @param claims 身份信息
     * @return 用户名
     */
    public static String getUserName(Claims claims)
    {
   
        return getValue(claims, DETAILS_USERNAME);
    }

    /**
     * 根据身份信息获取键值
     *
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public static String getValue(Claims claims, String key)
    {
   
        Object value = claims;
        String defaultValue = key;

        if (null == value)
        {
   
            return defaultValue;
        }
        if (value instanceof String)
        {
   
            return (String) value;
        }
        return value.toString();
    }
}

2.5.3 创建RedisCache类

package com.yunfeng.tokendemo.utils;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
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;

/**
 * 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);
    }
}

2.5.4 创建WebUtils类

package com.yunfeng.tokendemo.utils;

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

public class WebUtils {
   

    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response,String string){
   
        try {
   
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().println(string);
        } catch (IOException e) {
   
            e.printStackTrace();
        }
        return null;
    }
}

2.6 使用mybatis-plus连接数据库

参考:https://blog.csdn.net/weixin_43684214/article/details/128007977

3 实现UserDetailsService,重写loadUserByUsername方法,替换InMemoryUserDetailsManager的loadUserByUsername方法

3.1 在domain包下新建LoginUser并实现UserDetails

package com.yunfeng.tokendemo.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
   

    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
   
        return null;
    }

    @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;
    }
}

3.2 在service包下新建UserDetailsServiceImpl并实现UserDetailsService

package com.yunfeng.tokendemo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yunfeng.tokendemo.domain.LoginUser;
import com.yunfeng.tokendemo.domain.User;
import com.yunfeng.tokendemo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
   

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   
        //查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        //如果没有查询到用户就抛出异常
        if (Objects.isNull(user)) {
   
            throw new RuntimeException("用户名或者密码错误");
        }
        // TODO 查询对应的权限信息

        //把数据封装成UserDetails返回
        return new LoginUser(user);
    }
}

4 使用BCryptPasswordEncoder进行密码校验方式

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   

    @Bean
    public PasswordEncoder passwordEncoder(){
   
        return new BCryptPasswordEncoder();
    }
}

5 自定义登录接口

5.1 新建LoginController

package com.yunfeng.tokendemo.controller;

import com.yunfeng.tokendemo.domain.AjaxResult;
import com.yunfeng.tokendemo.domain.User;
import com.yunfeng.tokendemo.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {
   

    @Autowired
    private LoginService loginService;

    @PostMapping("/user/login")
    public AjaxResult login(@RequestBody User user){
   
        //登录
        return loginService.login(user);
    }

}

5.2 SecurityConfig中添加代码

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

    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     * <p>
     * Any endpoint that requires defense against common vulnerabilities can be specified
     * here, including public ones. See {@link HttpSecurity#authorizeRequests} and the
     * `permitAll()` authorization rule for more details on public endpoints.
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
   
        http    //CSRF禁用,因为不使用session
                .csrf().disable()
                //基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //授权请求
                .authorizeRequests()
                //对于登录login 允许匿名访问
                .antMatchers("/user/login").anonymous()
                //除上面所有请求都要鉴权认证
                .anyRequest().authenticated();
    }

5.3 新建接口LoginService和LoginServiceImpl实现类

  • 接口
    ```java
    package com.yunfeng.tokendemo.service;

import com.yunfeng.tokendemo.domain.AjaxResult;
import com.yunfeng.tokendemo.domain.User;

public interface LoginService {
AjaxResult login(User user);
}

* 实现类
```java
package com.yunfeng.tokendemo.service.impl;

import com.yunfeng.tokendemo.domain.AjaxResult;
import com.yunfeng.tokendemo.domain.LoginUser;
import com.yunfeng.tokendemo.domain.User;
import com.yunfeng.tokendemo.service.LoginService;
import com.yunfeng.tokendemo.utils.JwtUtils;
import com.yunfeng.tokendemo.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public AjaxResult login(User user) {
        AjaxResult ajax = AjaxResult.success();
        //AuthenticationManager authenticate 进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果认证没通过,给出对应的提示
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败");
        }
        //如果认证通过了,使用userid生成一个jwt jwt存入ajax
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        Long userId = loginUser.getUser().getId();
        Map<String,Object> map = new HashMap<>();
        map.put(JwtUtils.DETAILS_USER_ID,userId);
        String token = JwtUtils.createToken(map);
        ajax.put("token",token);
        //把完整的用户信息存入redis userid作为key
        redisCache.setCacheObject("login:"+userId,loginUser);
        return ajax;
    }
}

6 自定义JWT认证过滤器

6.1 新建JwtAuthenticationTokenFilter并实现OncePerRequestFilter

package com.yunfeng.tokendemo;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.yunfeng.tokendemo.domain.LoginUser;
import com.yunfeng.tokendemo.utils.JwtUtils;
import com.yunfeng.tokendemo.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
   

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
   
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
   
            //放行
            filterChain.doFilter(request,response);
            return;
        }
        //解析token
        String userId = JwtUtils.getUserId(token);
        //从redis中获取用户信息
        userId = userId.substring(userId.indexOf("=") + 1 , userId.indexOf("}"));
        String redisKey = "login:" + userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
   
            throw new RuntimeException("用户未登录!");
        }
        //存入SecurityContextHolder
        // TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        //放行
        filterChain.doFilter(request,response);
    }
}

6.2 添加JWT filter 到 UsernamePasswordAuthenticationFilter 之前

  • 在SecurityConfig中添加代码

      @Autowired
      private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
         
          http    //CSRF禁用,因为不使用session
                  .csrf().disable()
                  //基于token,所以不需要session
                  .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                  .and()
                  //授权请求
                  .authorizeRequests()
                  //对于登录login 允许匿名访问
                  .antMatchers("/user/login").anonymous()
                  //除上面所有请求都要鉴权认证
                  .anyRequest().authenticated();
    
          // 添加JWT filter 到 UsernamePasswordAuthenticationFilter 之前
          http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
      }
    

    7 退出登录

    7.1 新增登出方法

    @RequestMapping("/user/logout")
      public AjaxResult logout(){
         
          return loginService.logout();
      }
    

    7.2 新增登出逻辑

    @Override
      public AjaxResult logout() {
         
          //获取SecirityContextHolder中的用户id
          UsernamePasswordAuthenticationToken authentication =
                  (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
          LoginUser loginUser = (LoginUser) authentication.getPrincipal();
          Long id = loginUser.getUser().getId();
          //删除redis中的值
          redisCache.deleteObject("login:" + id);
          return AjaxResult.success();
      }
    

    8 授权实现

    8.1 LoginUser新增属性

    private List<String> permissions;
    
      @JSONField(serialize = false)
      private List<SimpleGrantedAuthority> authorities;
    
      public LoginUser(User user, List<String> permissions) {
         
          this.user = user;
          this.permissions = permissions;
      }
    
      @Override
      public Collection<? extends GrantedAuthority> getAuthorities() {
         
          if (authorities!=null){
         
              return authorities;
          }
          //把permissions中的String类型的权限信息封装成
          return permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
      }
    

    8.2 查询对应的权限信息

  • 在loadUserByUsername中设置权限信息
           //  查询对应的权限信息
          List<String> list = new ArrayList<>(Arrays.asList("test","admin"));
          //把数据封装成UserDetails返回
          return new LoginUser(user,list);
    

    8.3 JwtAuthenticationTokenFilter封装权限信息到Authentication中

          LoginUser loginUser = redisCache.getCacheObject(redisKey);
          if (Objects.isNull(loginUser)){
         
              throw new RuntimeException("用户未登录!");
          }
          //  获取权限信息封装到Authentication中
          UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                  new UsernamePasswordAuthenticationToken(loginUser,null, loginUser.getAuthorities());
    

    9 代码地址

    想看代码的可以看一下,都是简单的入门案例!
    https://gitee.com/logicfeng/springsecurity-jwt.git
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
7月前
|
安全 Java Spring
Spring Security+jwt实现认证
Spring Security+jwt实现认证
|
7月前
|
安全 数据安全/隐私保护
Springboot+Spring security +jwt认证+动态授权
Springboot+Spring security +jwt认证+动态授权
211 0
|
3月前
|
安全 Java 数据安全/隐私保护
|
4月前
|
NoSQL 关系型数据库 MySQL
SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘
SpringBoot 集成 SpringSecurity + MySQL + JWT 附源码,废话不多直接盘
159 2
|
缓存 安全 算法
Spring Security OAuth 2.0 资源服务器— JWT
Spring Security OAuth 2.0 资源服务器— JWT
586 1
|
6月前
|
JSON 安全 Java
Spring Security 与 JWT、OAuth 2.0 整合详解:构建安全可靠的认证与授权机制
Spring Security 与 JWT、OAuth 2.0 整合详解:构建安全可靠的认证与授权机制
537 0
|
7月前
|
安全 Java Spring
Spring Security整合JWT
该文档介绍了Spring Security与JWT的整合应用。在前后端分离的项目中,为了解决权限问题,通常采用Spring Security结合JWT的方案。文档涵盖了认证流程,包括同步认证和前后端分离认证,并详细说明了认证实现步骤,如环境准备、所需依赖(包括JWT库和Hutool工具包)的添加。此外,还提到从先前项目复制代码和配置以简化环境搭建。
218 6
|
7月前
|
安全 Java 数据库
SpringSecurity+JWT前后端分离架构登录认证
在SpringSecurity实现前后端分离登录token认证详解_springsecurity前后端分离登录认证-CSDN博客基础上进行重构,实现前后端分离架构登录认证,基本思想相同,借鉴开源Gitee代码进行改造,具有更好的代码规范。
346 1
|
7月前
|
安全 Java API
深度解析 Spring Security:身份验证、授权、OAuth2 和 JWT 身份验证的完整指南
Spring Security 是一个用于保护基于 Java 的应用程序的框架。它是一个功能强大且高度可定制的身份验证和访问控制框架,可以轻松地集成到各种应用程序中,包括 Web 应用程序和 RESTful Web 服务。 Spring Security 提供了全面的安全解决方案,用于身份验证和授权,并且可以用于在 Web 和方法级别上保护应用程序。
902 0
|
JSON 安全 Java
Spring Security + JWT使用
Spring Security + JWT使用

热门文章

最新文章