当前位置:   article > 正文

SpringBoot整合JWT+Spring Security+Redis实现登录拦截(一)登录认证_springboot redis和jwt防止用户重复登录视频教程

springboot redis和jwt防止用户重复登录视频教程

一、JWT简介

        JWT 全称 JSON Web Token,JWT 主要用于用户登录鉴权,当用户登录之后,返回给前端一个Token,之后用户利用Token进行信息交互。

       除了JWT认证之外,比较传统的还有Session认证,如何选择可以查看之前的博文:基于token与Session身份认证对比-CSDN博客简单来说认证就是让服务器知道你是谁?就是让服务器知道你能干什么,不能干什么?那么基于身份认证我们一般有俩种方式:Session-Cookie和JWT。https://blog.csdn.net/shaogaiyue9745602/article/details/135130114?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22135130114%22%2C%22source%22%3A%22shaogaiyue9745602%22%7D

二、Spring Security简介

        Spring Security 是基于 Spring 的身份认证(Authentication)和用户授权(Authorization)框架,提供了一套 Web 应用安全性的完整解决方案。其中核心技术使用了 Servlet 过滤器、IOC 和 AOP 等。实际操作时经常需要实现XXXFilter来自定义的登录以及访问控制。

  • 什么是身份认证

        身份认证指的是用户去访问系统资源时,系统要求验证用户的身份信息,用户身份合法才访问对应资源。常见的身份认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

  • 什么是用户授权

        当身份认证通过后,去访问系统的资源,系统会判断用户是否拥有访问该资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。比如 会员管理模块有增删改查功能,有的用户只能进行查询,而有的用户可以进行修改、删除。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

三、登录拦截流程

     其中认证的过程还应包含 鉴权 以及 token过期 验证。 

四、认证流程流程

五、代码实现

     代码实现前需要有springboot项目有基础的用户user表,并可以实现数据的查询功能。最好有自己的统一返回配置和全局异常处理配置,若没有可参考前面的博客参考进行配置或自行配置。

  1.在pom中添加相关依赖

  1. <!--安全框架引入, 进行权限控制-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>io.jsonwebtoken</groupId>
  8. <artifactId>jjwt</artifactId>
  9. <version>0.9.1</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>com.alibaba</groupId>
  13. <artifactId>fastjson</artifactId>
  14. <version>1.2.50</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>javax.xml.bind</groupId>
  18. <artifactId>jaxb-api</artifactId>
  19. <version>2.3.1</version>
  20. </dependency>

   引入依赖重新启动后,访问项目发现会出现登录页面如下:

        用户名默认为:user 

        默认密码在项目启动时会打印在控制台如下:

        也可通过在配置文件 application.properties 自定义用户名和密码

  1. spring.security.user.name=admin
  2. spring.security.user.password=admin

2.创建SecurityUser实现UserDetails

  1. package com.hng.config.jwtSecurity;
  2. import com.alibaba.fastjson.annotation.JSONField;
  3. import com.hng.entity.SysUser;
  4. import lombok.Data;
  5. import lombok.NoArgsConstructor;
  6. import org.springframework.security.core.GrantedAuthority;
  7. import org.springframework.security.core.authority.SimpleGrantedAuthority;
  8. import org.springframework.security.core.userdetails.UserDetails;
  9. import java.util.Collection;
  10. import java.util.List;
  11. /**
  12. * @Author: 郝南过
  13. * @Description:
  14. * @Date: 2023/11/29 10:26
  15. * @Version: 1.0
  16. */
  17. @Data
  18. @NoArgsConstructor
  19. public class SecurityUser implements UserDetails {
  20. private SysUser user;
  21. private List<String> permissions;
  22. @JSONField(serialize = false) // 防止存入redis时序列化出错,不进行序列化,也不存入redis中
  23. private List<SimpleGrantedAuthority> authorities;
  24. public SecurityUser(SysUser user) {
  25. this.user = user;
  26. }
  27. @Override
  28. public Collection<? extends GrantedAuthority> getAuthorities() {
  29. return null;
  30. }
  31. @Override
  32. public String getPassword() {
  33. return user.getPassword();
  34. }
  35. @Override
  36. public String getUsername() {
  37. return user.getUserName();
  38. }
  39. @Override
  40. public boolean isAccountNonExpired() {
  41. return true;
  42. }
  43. @Override
  44. public boolean isAccountNonLocked() {
  45. return true;
  46. }
  47. @Override
  48. public boolean isCredentialsNonExpired() {
  49. return true;
  50. }
  51. @Override
  52. public boolean isEnabled() {
  53. return true;
  54. }
  55. }

3.创建UserDetailsServiceImpl实现UserDetailsService

      该类主要功能是将Security拦截的用户名密码改为查询数据库中的用户名密码,以及权限的相关认证(权限认证相关本篇暂时不做添加

  1. package com.hng.config.jwtSecurity;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.hng.entity.SysUser;
  4. import com.hng.service.SysUserService;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.security.core.userdetails.UserDetails;
  7. import org.springframework.security.core.userdetails.UserDetailsService;
  8. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  9. import org.springframework.stereotype.Service;
  10. import javax.annotation.Resource;
  11. import java.util.Objects;
  12. /**
  13. * @Author: 郝南过
  14. * @Description: 将Security拦截的用户名密码改为数据库中已有的用户名密码,Security自己校验密码,默认使用PasswordEncoder,格式为{id}password(id代表加密方式),
  15. * 一般不采用此方式,SpringSecurity提供了BcryptPasswordEncoder,只需将此注入到spring容器中。SpringSecurity就会使用它进行替换校验
  16. * @Date: 2023/12/11 11:29
  17. * @Version: 1.0
  18. */
  19. @Service
  20. @Slf4j
  21. public class UserDetailsServiceImpl implements UserDetailsService {
  22. @Resource
  23. private SysUserService sysUserService;
  24. @Override
  25. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  26. //查询用户信息
  27. LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper();
  28. queryWrapper.eq(SysUser::getUserName,username);
  29. SysUser user = sysUserService.getOne(queryWrapper);
  30. if(Objects.isNull(user)){
  31. throw new RuntimeException("用户名或密码错误");
  32. }
  33. //TODO 查询授权信息
  34. return new SecurityUser(user);
  35. }
  36. }

4.创建JwtTokenUtil工具类

      该工具类用于token的创建验证等

  1. package com.hng.config.jwtSecurity;
  2. import io.jsonwebtoken.Claims;
  3. import io.jsonwebtoken.Jwts;
  4. import io.jsonwebtoken.SignatureAlgorithm;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.beans.factory.annotation.Value;
  7. import org.springframework.security.core.userdetails.UserDetails;
  8. import org.springframework.stereotype.Component;
  9. import javax.servlet.http.HttpServletRequest;
  10. import java.util.Date;
  11. import java.util.HashMap;
  12. import java.util.Map;
  13. /**
  14. * @Description JwtToken生成的工具类
  15. * @Version 1.0
  16. **/
  17. @Slf4j
  18. @Component
  19. public class JwtTokenUtil {
  20. private static final String CLAIM_KEY_USERNAME = "username";
  21. private static final String CLAIM_KEY_CREATED = "created";
  22. private static final String CLAIM_KEY_USER_ID = "userId";
  23. // 令牌自定义标识
  24. @Value("${jwt.token.header}")
  25. private String header;
  26. // 令牌秘钥
  27. @Value("${jwt.token.secret}")
  28. private String secret;
  29. // 令牌有效期(默认30分钟),也可将token的过期时间交给redis管理
  30. @Value("${jwt.token.expireTime}")
  31. private Long expiration;
  32. /**
  33. * 根据负责生成JWT的token
  34. */
  35. private String generateToken(Map<String, Object> claims) {
  36. return Jwts.builder()
  37. .setClaims(claims)
  38. .setExpiration(generateExpirationDate())
  39. .signWith(SignatureAlgorithm.HS512, secret)
  40. .compact();
  41. }
  42. /**
  43. * 从token中获取JWT中的负载
  44. */
  45. public Claims getClaimsFromToken(String token) {
  46. Claims claims = null;
  47. try {
  48. claims = Jwts.parser()
  49. .setSigningKey(secret)
  50. .parseClaimsJws(token)
  51. .getBody();
  52. } catch (Exception e) {
  53. log.info("JWT格式验证失败:{}",token);
  54. }
  55. return claims;
  56. }
  57. /**
  58. * 生成token的过期时间,返回Date
  59. */
  60. private Date generateExpirationDate() {
  61. return new Date(System.currentTimeMillis() + expiration * 1000);
  62. }
  63. /**
  64. * 从token中获取登录用户名
  65. */
  66. public String getUserNameFromToken(String token) {
  67. String username;
  68. try {
  69. Claims claims = getClaimsFromToken(token);
  70. username = claims.getSubject();
  71. } catch (Exception e) {
  72. username = null;
  73. }
  74. return username;
  75. }
  76. /**
  77. * 验证token是否还有效
  78. *
  79. * @param token 客户端传入的token
  80. * @param userDetails 从数据库中查询出来的用户信息
  81. */
  82. public boolean validateToken(String token, UserDetails userDetails) {
  83. String username = getUserNameFromToken(token);
  84. return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
  85. }
  86. /**
  87. * 判断token是否已经失效
  88. */
  89. public boolean isTokenExpired(String token) {
  90. Date expiredDate = getExpiredDateFromToken(token);
  91. return expiredDate.before(new Date());
  92. }
  93. /**
  94. * 从token中获取过期时间
  95. */
  96. private Date getExpiredDateFromToken(String token) {
  97. Claims claims = getClaimsFromToken(token);
  98. return claims.getExpiration();
  99. }
  100. /**
  101. * 根据用户信息生成token
  102. */
  103. public String generateToken(UserDetails userDetails) {
  104. Map<String, Object> claims = new HashMap<>();
  105. claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
  106. claims.put(CLAIM_KEY_CREATED, new Date());
  107. return generateToken(claims);
  108. }
  109. /**
  110. * 根据用户信息生成token
  111. */
  112. public String generateToken(SecurityUser securityUser) {
  113. Map<String, Object> claims = new HashMap<>();
  114. claims.put(CLAIM_KEY_USERNAME, securityUser.getUsername());
  115. claims.put(CLAIM_KEY_USER_ID, securityUser.getUser().getId());
  116. claims.put(CLAIM_KEY_CREATED, new Date());
  117. return generateToken(claims);
  118. }
  119. /**
  120. * 判断token是否可以被刷新
  121. */
  122. public boolean canRefresh(String token) {
  123. return !isTokenExpired(token);
  124. }
  125. /**
  126. * 刷新token
  127. */
  128. public String refreshToken(String token) {
  129. Claims claims = getClaimsFromToken(token);
  130. claims.put(CLAIM_KEY_CREATED, new Date());
  131. return generateToken(claims);
  132. }
  133. /**
  134. * 获取请求token
  135. *
  136. * @param request
  137. * @return token
  138. */
  139. public String getToken(HttpServletRequest request)
  140. {
  141. return request.getHeader(header);
  142. }
  143. }

5.创建RedisUtil工具类

       springboot如何集成redis可查看之前的博文

Springboot 集成Redis-CSDN博客文章浏览阅读621次,点赞9次,收藏9次。注意commons-pool2包与spring的版本一致性,若出错尝试升级或降级commons-pool2版本。https://blog.csdn.net/shaogaiyue9745602/article/details/134669420?spm=1001.2014.3001.5501

      记得在配置文件application.properties中添加redis配置信息

  1. spring.redis.host=127.0.0.1
  2. spring.redis.host.port=6379
  3. #spring.redis.host.name=
  4. #spring.redis.host.password=
  RedisUtil.java
  1. package com.hng.config.redis;
  2. import org.springframework.core.annotation.Order;
  3. import org.springframework.data.redis.core.RedisTemplate;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.util.CollectionUtils;
  6. import javax.annotation.Resource;
  7. import java.util.List;
  8. import java.util.Map;
  9. import java.util.Set;
  10. import java.util.concurrent.TimeUnit;
  11. @Component
  12. @Order(-1)
  13. public final class RedisUtil {
  14. @Resource
  15. private RedisTemplate redisTemplate;
  16. /**
  17. * 指定缓存失效时间
  18. *
  19. * @param key 键
  20. * @param time 时间(秒)
  21. * @return 0
  22. */
  23. public boolean expire(String key, long time) {
  24. try {
  25. if (time > 0) {
  26. redisTemplate.expire(key, time, TimeUnit.SECONDS);
  27. }
  28. return true;
  29. } catch (Exception e) {
  30. e.printStackTrace();
  31. return false;
  32. }
  33. }
  34. /**
  35. * 根据key 获取过期时间
  36. *
  37. * @param key 键 不能为null
  38. * @return 时间(秒) 返回0代表为永久有效
  39. */
  40. public long getExpire(String key) {
  41. return redisTemplate.getExpire(key, TimeUnit.SECONDS);
  42. }
  43. /**
  44. * 判断key是否存在
  45. *
  46. * @param key 键
  47. * @return true 存在 false不存在
  48. */
  49. public boolean hasKey(String key) {
  50. try {
  51. return redisTemplate.hasKey(key);
  52. } catch (Exception e) {
  53. e.printStackTrace();
  54. return false;
  55. }
  56. }
  57. /**
  58. * 删除缓存
  59. *
  60. * @param key 可以传一个值 或多个
  61. */
  62. @SuppressWarnings("unchecked")
  63. public void del(String... key) {
  64. if (key != null && key.length > 0) {
  65. if (key.length == 1) {
  66. redisTemplate.delete(key[0]);
  67. } else {
  68. redisTemplate.delete(CollectionUtils.arrayToList(key));
  69. }
  70. }
  71. }
  72. // ============================String=============================
  73. /**
  74. * 普通缓存获取
  75. *
  76. * @param key 键
  77. * @return 值
  78. */
  79. public Object get(String key) {
  80. return key == null ? null : redisTemplate.opsForValue().get(key);
  81. }
  82. /**
  83. * 普通缓存放入
  84. *
  85. * @param key 键
  86. * @param value 值
  87. * @return true成功 false失败
  88. */
  89. public boolean set(String key, Object value) {
  90. try {
  91. redisTemplate.opsForValue().set(key, value);
  92. return true;
  93. } catch (Exception e) {
  94. e.printStackTrace();
  95. return false;
  96. }
  97. }
  98. /**
  99. * 普通缓存放入并设置时间
  100. *
  101. * @param key 键
  102. * @param value 值
  103. * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
  104. * @return true成功 false 失败
  105. */
  106. public boolean set(String key, Object value, long time) {
  107. try {
  108. if (time > 0) {
  109. redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
  110. } else {
  111. set(key, value);
  112. }
  113. return true;
  114. } catch (Exception e) {
  115. e.printStackTrace();
  116. return false;
  117. }
  118. }
  119. /**
  120. * 递增
  121. *
  122. * @param key 键
  123. * @param delta 要增加几(大于0)
  124. * @return
  125. */
  126. public long incr(String key, long delta) {
  127. if (delta < 0) {
  128. throw new RuntimeException("递增因子必须大于0");
  129. }
  130. return redisTemplate.opsForValue().increment(key, delta);
  131. }
  132. /**
  133. * 递减
  134. *
  135. * @param key 键
  136. * @param delta 要减少几(小于0)
  137. * @return
  138. */
  139. public long decr(String key, long delta) {
  140. if (delta < 0) {
  141. throw new RuntimeException("递减因子必须大于0");
  142. }
  143. return redisTemplate.opsForValue().increment(key, -delta);
  144. }
  145. // ================================Map=================================
  146. /**
  147. * HashGet
  148. *
  149. * @param key 键 不能为null
  150. * @param item 项 不能为null
  151. * @return 值
  152. */
  153. public Object hget(String key, String item) {
  154. return redisTemplate.opsForHash().get(key, item);
  155. }
  156. /**
  157. * 获取hashKey对应的所有键值
  158. *
  159. * @param key 键
  160. * @return 对应的多个键值
  161. */
  162. public Map<Object, Object> hmget(String key) {
  163. return redisTemplate.opsForHash().entries(key);
  164. }
  165. /**
  166. * HashSet
  167. *
  168. * @param key 键
  169. * @param map 对应多个键值
  170. * @return true 成功 false 失败
  171. */
  172. public boolean hmset(String key, Map<String, Object> map) {
  173. try {
  174. redisTemplate.opsForHash().putAll(key, map);
  175. return true;
  176. } catch (Exception e) {
  177. e.printStackTrace();
  178. return false;
  179. }
  180. }
  181. /**
  182. * HashSet 并设置时间
  183. *
  184. * @param key 键
  185. * @param map 对应多个键值
  186. * @param time 时间(秒)
  187. * @return true成功 false失败
  188. */
  189. public boolean hmset(String key, Map<String, Object> map, long time) {
  190. try {
  191. redisTemplate.opsForHash().putAll(key, map);
  192. if (time > 0) {
  193. expire(key, time);
  194. }
  195. return true;
  196. } catch (Exception e) {
  197. e.printStackTrace();
  198. return false;
  199. }
  200. }
  201. /**
  202. * 向一张hash表中放入数据,如果不存在将创建
  203. *
  204. * @param key 键
  205. * @param item 项
  206. * @param value 值
  207. * @return true 成功 false失败
  208. */
  209. public boolean hset(String key, String item, Object value) {
  210. try {
  211. redisTemplate.opsForHash().put(key, item, value);
  212. return true;
  213. } catch (Exception e) {
  214. e.printStackTrace();
  215. return false;
  216. }
  217. }
  218. /**
  219. * 向一张hash表中放入数据,如果不存在将创建
  220. * 0
  221. *
  222. * @param key 键
  223. * @param item 项
  224. * @param value 值
  225. * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
  226. * @return true 成功 false失败
  227. */
  228. public boolean hset(String key, String item, Object value, long time) {
  229. try {
  230. redisTemplate.opsForHash().put(key, item, value);
  231. if (time > 0) {
  232. expire(key, time);
  233. }
  234. return true;
  235. } catch (Exception e) {
  236. e.printStackTrace();
  237. return false;
  238. }
  239. }
  240. /**
  241. * 删除hash表中的值
  242. *
  243. * @param key 键 不能为null
  244. * @param item 项 可以使多个 不能为null
  245. */
  246. public void hdel(String key, Object... item) {
  247. redisTemplate.opsForHash().delete(key, item);
  248. }
  249. /**
  250. * 判断hash表中是否有该项的值
  251. *
  252. * @param key 键 不能为null
  253. * @param item 项 不能为null
  254. * @return true 存在 false不存在
  255. */
  256. public boolean hHasKey(String key, String item) {
  257. return redisTemplate.opsForHash().hasKey(key, item);
  258. }
  259. /**
  260. * hash递增 如果不存在,就会创建一个 并把新增后的值返回
  261. *
  262. * @param key 键
  263. * @param item 项
  264. * @param by 要增加几(大于0)
  265. * @return
  266. */
  267. public double hincr(String key, String item, double by) {
  268. return redisTemplate.opsForHash().increment(key, item, by);
  269. }
  270. /**
  271. * hash递减
  272. *
  273. * @param key 键
  274. * @param item 项
  275. * @param by 要减少记(小于0)
  276. * @return
  277. */
  278. public double hdecr(String key, String item, double by) {
  279. return redisTemplate.opsForHash().increment(key, item, -by);
  280. }
  281. // ============================set=============================
  282. /**
  283. * 根据key获取Set中的所有值
  284. *
  285. * @param key 键
  286. * @return
  287. */
  288. public Set<Object> sGet(String key) {
  289. try {
  290. return redisTemplate.opsForSet().members(key);
  291. } catch (Exception e) {
  292. e.printStackTrace();
  293. return null;
  294. }
  295. }
  296. /**
  297. * 根据value从一个set中查询,是否存在
  298. *
  299. * @param key 键
  300. * @param value 值
  301. * @return true 存在 false不存在
  302. */
  303. public boolean sHasKey(String key, Object value) {
  304. try {
  305. return redisTemplate.opsForSet().isMember(key, value);
  306. } catch (Exception e) {
  307. e.printStackTrace();
  308. return false;
  309. }
  310. }
  311. /**
  312. * 将数据放入set缓存
  313. *
  314. * @param key 键
  315. * @param values 值 可以是多个
  316. * @return 成功个数
  317. */
  318. public long sSet(String key, Object... values) {
  319. try {
  320. return redisTemplate.opsForSet().add(key, values);
  321. } catch (Exception e) {
  322. e.printStackTrace();
  323. return 0;
  324. }
  325. }
  326. /**
  327. * 将set数据放入缓存
  328. *
  329. * @param key 键
  330. * @param time 时间(秒)
  331. * @param values 值 可以是多个
  332. * @return 成功个数
  333. */
  334. public long sSetAndTime(String key, long time, Object... values) {
  335. try {
  336. Long count = redisTemplate.opsForSet().add(key, values);
  337. if (time > 0)
  338. expire(key, time);
  339. return count;
  340. } catch (Exception e) {
  341. e.printStackTrace();
  342. return 0;
  343. }
  344. }
  345. /**
  346. * 获取set缓存的长度
  347. *
  348. * @param key 键
  349. * @return
  350. */
  351. public long sGetSetSize(String key) {
  352. try {
  353. return redisTemplate.opsForSet().size(key);
  354. } catch (Exception e) {
  355. e.printStackTrace();
  356. return 0;
  357. }
  358. }
  359. /**
  360. * 移除值为value的
  361. *
  362. * @param key 键
  363. * @param values 值 可以是多个
  364. * @return 移除的个数
  365. */
  366. public long setRemove(String key, Object... values) {
  367. try {
  368. Long count = redisTemplate.opsForSet().remove(key, values);
  369. return count;
  370. } catch (Exception e) {
  371. e.printStackTrace();
  372. return 0;
  373. }
  374. }
  375. // ===============================list=================================
  376. /**
  377. * 获取list缓存的内容
  378. *
  379. * @param key 键
  380. * @param start 开始
  381. * @param end 结束 0 到 -代表所有值
  382. * @return
  383. */
  384. public List<Object> lGet(String key, long start, long end) {
  385. try {
  386. return redisTemplate.opsForList().range(key, start, end);
  387. } catch (Exception e) {
  388. e.printStackTrace();
  389. return null;
  390. }
  391. }
  392. /**
  393. * 获取list缓存的长度
  394. *
  395. * @param key 键
  396. * @return 0
  397. */
  398. public long lGetListSize(String key) {
  399. try {
  400. return redisTemplate.opsForList().size(key);
  401. } catch (Exception e) {
  402. e.printStackTrace();
  403. return 0;
  404. }
  405. }
  406. /**
  407. * 通过索引 获取list中的值
  408. *
  409. * @param key 键
  410. * @param index 索引 index>=0时, 0 表头, 第二个元素,依次类推;index<0时,-,表尾,-倒数第二个元素,依次类推
  411. * @return 0
  412. */
  413. public Object lGetIndex(String key, long index) {
  414. try {
  415. return redisTemplate.opsForList().index(key, index);
  416. } catch (Exception e) {
  417. e.printStackTrace();
  418. return null;
  419. }
  420. }
  421. /**
  422. * 将list放入缓存
  423. *
  424. * @param key 键
  425. * @param value 值
  426. * @return
  427. */
  428. public boolean lSet(String key, Object value) {
  429. try {
  430. redisTemplate.opsForList().rightPush(key, value);
  431. return true;
  432. } catch (Exception e) {
  433. e.printStackTrace();
  434. return false;
  435. }
  436. }
  437. /**
  438. * 将list放入缓存
  439. *
  440. * @param key 键
  441. * @param value 值
  442. * @param time 时间(秒)
  443. * @return
  444. */
  445. public boolean lSet(String key, Object value, long time) {
  446. try {
  447. redisTemplate.opsForList().rightPush(key, value);
  448. if (time > 0)
  449. expire(key, time);
  450. return true;
  451. } catch (Exception e) {
  452. e.printStackTrace();
  453. return false;
  454. }
  455. }
  456. /**
  457. * 将list放入缓存
  458. *
  459. * @param key 键
  460. * @param value 值
  461. * @return
  462. */
  463. public boolean lSet(String key, List<Object> value) {
  464. try {
  465. redisTemplate.opsForList().rightPushAll(key, value);
  466. return true;
  467. } catch (Exception e) {
  468. e.printStackTrace();
  469. return false;
  470. }
  471. }
  472. /**
  473. * 将list放入缓存
  474. *
  475. * @param key 键
  476. * @param value 值
  477. * @param time 时间(秒)
  478. * @return 0
  479. */
  480. public boolean lSet(String key, List<Object> value, long time) {
  481. try {
  482. redisTemplate.opsForList().rightPushAll(key, value);
  483. if (time > 0)
  484. expire(key, time);
  485. return true;
  486. } catch (Exception e) {
  487. e.printStackTrace();
  488. return false;
  489. }
  490. }
  491. /**
  492. * 根据索引修改list中的某条数据
  493. *
  494. * @param key 键
  495. * @param index 索引
  496. * @param value 值
  497. * @return 0
  498. */
  499. public boolean lUpdateIndex(String key, long index, Object value) {
  500. try {
  501. redisTemplate.opsForList().set(key, index, value);
  502. return true;
  503. } catch (Exception e) {
  504. e.printStackTrace();
  505. return false;
  506. }
  507. }
  508. /**
  509. * 移除N个值为value
  510. *
  511. * @param key 键
  512. * @param count 移除多少个
  513. * @param value 值
  514. * @return 移除的个数
  515. */
  516. public long lRemove(String key, long count, Object value) {
  517. try {
  518. Long remove = redisTemplate.opsForList().remove(key, count, value);
  519. return remove;
  520. } catch (Exception e) {
  521. e.printStackTrace();
  522. return 0;
  523. }
  524. }
  525. }

6. 创建JwtAuthenticationTokenFilter实现OncePerRequestFilter

        该类为登录授权过滤器

  1. package com.hng.config.jwtSecurity;
  2. import com.hng.config.redis.RedisUtil;
  3. import io.jsonwebtoken.Claims;
  4. import org.springframework.beans.factory.annotation.Qualifier;
  5. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  6. import org.springframework.security.core.context.SecurityContextHolder;
  7. import org.springframework.stereotype.Component;
  8. import org.springframework.util.StringUtils;
  9. import org.springframework.web.filter.OncePerRequestFilter;
  10. import org.springframework.web.servlet.HandlerExceptionResolver;
  11. import javax.annotation.Resource;
  12. import javax.servlet.FilterChain;
  13. import javax.servlet.ServletException;
  14. import javax.servlet.http.HttpServletRequest;
  15. import javax.servlet.http.HttpServletResponse;
  16. import java.io.IOException;
  17. import java.util.Objects;
  18. /**
  19. * @Author: 郝南过
  20. * @Description: JWT 登录授权过滤器
  21. * @Date: 2023/11/28 16:19
  22. * @Version: 1.0
  23. */
  24. @Component
  25. public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
  26. @Resource
  27. private RedisUtil redisUtil;
  28. // JWT 工具类
  29. @Resource
  30. private JwtTokenUtil jwtTokenUtil;
  31. @Resource
  32. @Qualifier("handlerExceptionResolver")
  33. private HandlerExceptionResolver resolver;
  34. /**
  35. * 从请求中获取 JWT 令牌,并根据令牌获取用户信息,最后将用户信息封装到 Authentication 中,
  36. * 方便后续校验(只会执行一次)
  37. * @param request
  38. * @param response
  39. * @param filterChain
  40. * @throws ServletException
  41. * @throws IOException
  42. */
  43. @Override
  44. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  45. //获取token
  46. String token = request.getHeader("token");
  47. if (!StringUtils.hasText(token)) {
  48. //token为空的话, 就不管它, 让SpringSecurity中的其他过滤器处理请求,请求放行
  49. filterChain.doFilter(request, response);
  50. return;
  51. }
  52. //token不为空时, 解析token
  53. String userId = null;
  54. try {
  55. Claims claims = jwtTokenUtil.getClaimsFromToken(token);
  56. //解析出userId
  57. userId = claims.get("userId", String.class);
  58. } catch (Exception e) {
  59. e.printStackTrace();
  60. // 交给全局异常处理类处理
  61. throw new RuntimeException("token非法");
  62. // resolver.resolveException(request, response, null,new RuntimeException("token非法"));
  63. }
  64. //使用userId从Redis缓存中获取用户信息
  65. String redisKey = "login:" + userId;
  66. SecurityUser securityUser = (SecurityUser)redisUtil.get(redisKey);
  67. if (Objects.isNull(securityUser)) {
  68. throw new RuntimeException("用户未登录");
  69. // 交给全局异常处理类处理
  70. // resolver.resolveException(request, response, null,new RuntimeException("用户未登录"));
  71. }
  72. //将用户安全信息存入SecurityContextHolder, 在之后SpringSecurity的过滤器就不会拦截
  73. UsernamePasswordAuthenticationToken authenticationToken =
  74. new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
  75. SecurityContextHolder.getContext().setAuthentication(authenticationToken);
  76. //放行
  77. filterChain.doFilter(request, response);
  78. }
  79. }

7.创建SecurityConfig继承WebSecurityConfigurerAdapter

        该类主要为Security配置类

  1. package com.hng.config.jwtSecurity;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.security.authentication.AuthenticationManager;
  6. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  7. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  8. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  9. import org.springframework.security.config.http.SessionCreationPolicy;
  10. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  11. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  12. import javax.annotation.Resource;
  13. /**
  14. * @Author: 郝南过
  15. * @Description: TODO
  16. * @Date: 2023/11/28 16:24
  17. * @Version: 1.0
  18. */
  19. @Configuration //注册为SpringBoot的配置类
  20. @Slf4j
  21. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  22. //注入Jwt认证拦截器.
  23. @Resource
  24. private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
  25. /**
  26. * 将BCryptPasswordEncoder加密器注入SpringSecurity中,
  27. * SpringSecurity的DaoAuthenticaionProvider会调用该加密器中的match()方法进行密码比对, 密码比对过程不需要我们干涉
  28. * @return
  29. */
  30. @Bean
  31. public BCryptPasswordEncoder bcryptPasswordBean(){
  32. return new BCryptPasswordEncoder();
  33. }
  34. /**
  35. * 注入身份验证管理器, 直接继承即可.
  36. * @return
  37. * @throws Exception
  38. */
  39. @Bean
  40. @Override
  41. public AuthenticationManager authenticationManagerBean() throws Exception {
  42. return super.authenticationManagerBean();
  43. }
  44. /**
  45. * 配置
  46. * @param http
  47. * @throws Exception
  48. */
  49. @Override
  50. protected void configure(HttpSecurity http) throws Exception {
  51. http
  52. //禁用跨站请求伪造
  53. .csrf().disable()
  54. //禁用Session,使用token作为信息传递介质
  55. .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  56. .and()
  57. //把token校验过滤器添加到过滤器链中, 添加在UsernamePasswordAuthenticationFilter之前是因为只要用户携带token,
  58. //就不需要再去验证是否有用户名密码了 (而且我们不使用表单登入, UsernamePasswordAuthenticationFilter是无法解析Json的, 相当于它没用了)
  59. //UsernamePasswordAuthenticationFilter是SpringSecurity默认配置的表单登录拦截器
  60. .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
  61. // 认证请求的配置
  62. .authorizeRequests()
  63. // 将登入和注册的接口放开
  64. .antMatchers("/sys/login").anonymous()
  65. .antMatchers("/sys/register").anonymous()
  66. //除了上面的那些, 剩下的任何接口请求都需要经过认证
  67. .anyRequest().authenticated()
  68. .and()
  69. //允许跨域请求
  70. .cors();
  71. }
  72. }

8.在配置文件application.properties中添加相关的JWT配置信息

  1. # 令牌自定义标识
  2. jwt.token.header=token
  3. # 令牌秘钥
  4. jwt.token.secret=ZmQ0ZGI5NjQ0MDQwY2I4MjMxY2Y3ZmI3MjdhN2ZmMjNhODViOTg1ZGE0NTBjMGM4NDA5NzYxMjdjOWMwYWRmZTBlZjlhNGY3ZTg4Y2U3YTE1ODVkZDU5Y2Y3OGYwZWE1NzUzNWQ2YjFjZDc0NGMxZWU2MmQ3MjY1NzJmNTE0MzI=
  5. # 令牌有效期(默认30分钟)
  6. jwt.token.expireTime=1800

9.创建LoginController 

  1. package com.hng.controller;
  2. import com.hng.config.redis.RedisUtil;
  3. import com.hng.config.jwtSecurity.JwtTokenUtil;
  4. import com.hng.config.jwtSecurity.SecurityUser;
  5. import com.hng.entity.SysUser;
  6. import com.hng.service.SysUserService;
  7. import io.swagger.annotations.Api;
  8. import io.swagger.annotations.ApiOperation;
  9. import lombok.extern.slf4j.Slf4j;
  10. import org.springframework.http.HttpStatus;
  11. import org.springframework.http.ResponseEntity;
  12. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  13. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  14. import org.springframework.security.core.Authentication;
  15. import org.springframework.security.core.context.SecurityContextHolder;
  16. import org.springframework.security.crypto.password.PasswordEncoder;
  17. import org.springframework.web.bind.annotation.*;
  18. import javax.annotation.Resource;
  19. import java.util.HashMap;
  20. import java.util.Map;
  21. /**
  22. * @Author: 郝南过
  23. * @Description: TODO
  24. * @Date: 2023/11/29 10:20
  25. * @Version: 1.0
  26. */
  27. @Slf4j
  28. @RestController
  29. @RequestMapping("/sys/")
  30. @Api(value = "LoginController", tags = "LoginController")
  31. public class LoginController {
  32. @Resource
  33. private PasswordEncoder passwordEncoder;
  34. @Resource
  35. private AuthenticationManagerBuilder authenticationManagerBuilder;
  36. @Resource
  37. private JwtTokenUtil jwtTokenUtil;
  38. @Resource
  39. private RedisUtil redisUtil;
  40. @Resource
  41. private SysUserService sysUserService;
  42. @ApiOperation("用户登录")
  43. @PostMapping("/login")
  44. @ResponseBody
  45. public ResponseEntity<Object> userLogin(@RequestBody SysUser user) {
  46. UsernamePasswordAuthenticationToken authenticationToken =
  47. new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
  48. Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
  49. SecurityContextHolder.getContext().setAuthentication(authentication);
  50. //生成token
  51. SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
  52. String userId = securityUser.getUser().getId();
  53. String jwtToken = jwtTokenUtil.generateToken(securityUser);
  54. //redis保存用户信息
  55. boolean resultRedis = redisUtil.set("login:" + userId, securityUser);
  56. if(!resultRedis){
  57. throw new RuntimeException("redis连接不上,登录失败");
  58. }
  59. // 返回 token 与 用户信息
  60. Map<String, Object> authInfo = new HashMap<String, Object>(2) {{
  61. put("token", jwtToken);
  62. put("user", securityUser);
  63. }};
  64. return ResponseEntity.ok(authInfo);
  65. }
  66. @ApiOperation("用户注册")
  67. @PostMapping("/register")
  68. @ResponseBody
  69. public ResponseEntity<Object> userRegister(@RequestBody SysUser user){
  70. user.setPassword(passwordEncoder.encode("123456"));
  71. sysUserService.addSysUser(user);
  72. return new ResponseEntity<>(HttpStatus.CREATED);
  73. }
  74. @ApiOperation("退出登录")
  75. @GetMapping(value = "/logout")
  76. public ResponseEntity<Object> logout() {
  77. UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
  78. SecurityUser securityUser = (SecurityUser)authenticationToken.getPrincipal();
  79. redisUtil.del("login:" + securityUser.getUser().getId());
  80. return new ResponseEntity<>(HttpStatus.OK);
  81. }
  82. }

六、测试

        使用postman进行测试

      1.login登录获取token

2.getList获取数据

        将login中获取的token复制到headers中,请求测试

到此登录拦截已经可以正常使用了

七 、配置登录异常处理器

  1.创建JwtAuthenticationEntryPoint实现AuthenticationEntryPoint

        该类主要用来处理认证失败异常

  1. package com.hng.config.security;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.hng.config.response.ResponseResult;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.http.HttpStatus;
  6. import org.springframework.security.core.AuthenticationException;
  7. import org.springframework.security.web.AuthenticationEntryPoint;
  8. import org.springframework.stereotype.Component;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletResponse;
  11. import java.io.IOException;
  12. /**
  13. * @Author: 郝南过
  14. * @Description: 认证失败处理器
  15. * @Date: 2023/12/19 17:17
  16. * @Version: 1.0
  17. */
  18. @Component
  19. @Slf4j
  20. public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
  21. @Override
  22. public void commence(HttpServletRequest request,
  23. HttpServletResponse response,
  24. AuthenticationException authException) throws IOException {
  25. // 当用户尝试访问安全的REST资源而不提供任何凭据时,将调用此方法发送401 响应
  26. response.setContentType("application/json;charset=UTF-8");
  27. response.setStatus(HttpStatus.OK.value());
  28. response.getWriter().write(JSONObject.toJSONString(ResponseResult.fail(HttpStatus.UNAUTHORIZED.value(),"认证失败")));
  29. response.getWriter().flush();
  30. }
  31. }

2.创建JwtAccessDeniedHandler实现AccessDeniedHandler

        该类主要用于处理鉴权失败异常

  1. package com.hng.config.security;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.hng.config.response.ResponseResult;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.http.HttpStatus;
  6. import org.springframework.security.access.AccessDeniedException;
  7. import org.springframework.security.web.access.AccessDeniedHandler;
  8. import org.springframework.stereotype.Component;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletResponse;
  11. import java.io.IOException;
  12. /**
  13. * @Author: 郝南过
  14. * @Description: 鉴权失败处理
  15. * @Date: 2023/12/19 17:26
  16. * @Version: 1.0
  17. */
  18. @Component
  19. @Slf4j
  20. public class JwtAccessDeniedHandler implements AccessDeniedHandler {
  21. @Override
  22. public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
  23. //当用户在没有授权的情况下访问受保护的REST资源时,将调用此方法发送403 Forbidden响应
  24. response.setContentType("application/json;charset=UTF-8");
  25. response.setStatus(HttpStatus.OK.value());
  26. response.getWriter().write(JSONObject.toJSONString(ResponseResult.fail(HttpStatus.FORBIDDEN.value(),"鉴权失败")));
  27. response.getWriter().flush();
  28. }
  29. }

3.全局异常处理器GlobalExceptionHandler中配置以上俩个异常

  1. /**
  2. * 全局捕获security的权限不足异常
  3. */
  4. @ExceptionHandler(value = AccessDeniedException.class)
  5. public void accessDeniedException(AccessDeniedException e) {
  6. log.error("权限不足异常!原因是:[{}]", e.getMessage());
  7. throw e;
  8. }
  9. /**
  10. * 全局捕获security的认证失败异常
  11. */
  12. @ExceptionHandler(value = AuthenticationException.class)
  13. public void authenticationException(AuthenticationException e) {
  14. log.error("用户认证失败异常!原因是:[{}]", e.getMessage());
  15. throw e;
  16. }

4.在SecurityConfig中配置以上俩个异常

  1. @Resource
  2. private JwtAuthenticationEntryPoint authenticationEntryPoint;
  3. @Resource
  4. private JwtAccessDeniedHandler accessDeniedHandler;
  1. //配置异常处理器
  2. .exceptionHandling()
  3. //认证失败处理器
  4. .authenticationEntryPoint(authenticationEntryPoint)
  5. //鉴权失败处理器
  6. .accessDeniedHandler(accessDeniedHandler)

5.测试

        认证失败异常可使用错误的用户名密码登录进行测试

        鉴权失败异常,暂时不能测试,需要补全权限代码后才可以进行测试

【demo示例代码】

https://download.csdn.net/download/shaogaiyue9745602/88661442icon-default.png?t=N7T8https://download.csdn.net/download/shaogaiyue9745602/88661442

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号