丽水市建设局网站搜索热门关键词
文章目录
- 1 简介
- 1.1 认证和授权
- 1.2 spring security的核心过滤器链
- 1.3 入门案例认证流程
- 1.4 自定义SpringSecurity认证、授权、校验
- 1.4.1 登录
- 1.4.2 校验
- 2 快速入门
- 2.1 pom引入依赖
- 2.2 创建domain包
- 2.2.1 新建AjaxResult
- 2.2.2 新建User类
- 2.3 创建common包
- 2.3.1 创建HttpStatus类
- 2.4 创建config包
- 2.4.1 创建RedisConfig类
- 2.5 创建utils包
- 2.5.1 创建FastJson2JsonRedisSerializer包
- 2.5.2 创建JwtUtils类
- 2.5.3 创建RedisCache类
- 2.5.4 创建WebUtils类
- 2.6 使用mybatis-plus连接数据库
- 3 实现UserDetailsService,重写loadUserByUsername方法,替换InMemoryUserDetailsManager的loadUserByUsername方法
- 3.1 在domain包下新建LoginUser并实现UserDetails
- 3.2 在service包下新建UserDetailsServiceImpl并实现UserDetailsService
- 4 使用BCryptPasswordEncoder进行密码校验方式
- 5 自定义登录接口
- 5.1 新建LoginController
- 5.2 SecurityConfig中添加代码
- 5.3 新建接口LoginService和LoginServiceImpl实现类
- 6 自定义JWT认证过滤器
- 6.1 新建JwtAuthenticationTokenFilter并实现OncePerRequestFilter
- 6.2 添加JWT filter 到 UsernamePasswordAuthenticationFilter 之前
- 7 退出登录
- 7.1 新增登出方法
- 7.2 新增登出逻辑
- 8 授权实现
- 8.1 LoginUser新增属性
- 8.2 查询对应的权限信息
- 8.3 JwtAuthenticationTokenFilter封装权限信息到Authentication中
- 9 代码地址
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
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<String, Object>
{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 数据对象*/@Overridepublic AjaxResult put(String key, Object value){super.put(key, value);return this;}
}
2.2.2 新建User类
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类
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;}@Beanpublic 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;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic 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
{@Autowiredpublic 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;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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 {@Autowiredprivate UserMapper userMapper;@Overridepublic 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 {@Beanpublic 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 {@Autowiredprivate LoginService loginService;@PostMapping("/user/login")public AjaxResult login(@RequestBody User user){//登录return loginService.login(user);}}
5.2 SecurityConfig中添加代码
@Override@Beanpublic 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*/@Overrideprotected 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实现类
- 接口
package com.yunfeng.tokendemo.service;import com.yunfeng.tokendemo.domain.AjaxResult;
import com.yunfeng.tokendemo.domain.User;public interface LoginService {AjaxResult login(User user);
}
- 实现类
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 {@Resourceprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisCache redisCache;@Overridepublic 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存入ajaxLoginUser 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作为keyredisCache.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 {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {//放行filterChain.doFilter(request,response);return;}//解析tokenString 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中添加代码
@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Overrideprotected 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 新增登出逻辑
@Overridepublic AjaxResult logout() {//获取SecirityContextHolder中的用户idUsernamePasswordAuthenticationToken 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;}@Overridepublic 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