@[TOC]
1 简介
1.1 认证和授权
认证和授权是SpringSecurity
作为安全框架的核心功能
- 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
- 授权:经过认证后判断当前用户是否有权限进行某个操作
- 校验:再次访问时,判断是否有token
1.2 spring security的核心过滤器链
UsernamePasswordAuthenticationFilter
:负责处理我们在登录页面填写了用户名密码后的登录请求。ExceptionTranslationFilter
:处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
。FilterSecurityInterceptor
:负责权限校验的过滤器。1.3 入门案例认证流程
- 1、
UsernamePasswordAuthenticationFilter
将用户名和密码封装于Authentication
,此时,对象内只有用户名和密码,还没有权限信息 - 2、调用
authenticate
方法进行认证,传到ProviderManager
- 3、再调用
DaoAuthenticationProvider
的authenticate
方法进行认证 - 4、再调用
InMemoryUserDetailsManager
的loadUserByUsername
方法去内存中查询用户及其权限,并将用户信息和权限封装成UserDetails
对象返回到DaoAuthenticationProvider
- 5、通过
PasswordEncoder
对比UserDetails
和Authentication
的密码是否正确,如果正确就把权限信息设置到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