기술나눔

SpringSecurity 프레임워크 [인증]

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

목차

1. 빠른 시작

2. 인증

2.1 로그인 인증 절차

2.2 원리의 예비 탐구

2.3 문제 해결

2.3.1 아이디어 분석

2.3.2 준비

2.3.3 구현

2.3.3.1 데이터베이스 검증 사용자

2.3.3.2 비밀번호 암호화 저장

2.3.3.3 로그인 인터페이스

2.3.3.4 인증 필터

2.3.3.5 로그아웃


Spring Security는 Spring 계열의 보안 관리 프레임워크로, 또 다른 보안 프레임워크인 Shiro에 비해 Shiro보다 풍부한 기능과 풍부한 커뮤니티 리소스를 제공합니다.

일반적으로 Spring Security는 대규모 프로젝트에서 더 일반적으로 사용되며 Shiro는 소규모 프로젝트에서 더 일반적으로 사용됩니다. 왜냐하면 Spring Security에 비해 Shiro가 시작하기 더 쉽기 때문입니다.

일반 웹 애플리케이션에는 다음이 필요합니다.인증그리고승인하다

  • 인증: 현재 시스템에 접속하고 있는 사용자가 시스템 사용자인지 확인하고, 어떤 사용자인지 확인합니다.
  • 권한 부여: 인증 후 현재 사용자에게 작업 수행 권한이 있는지 확인

인증과 권한 부여는 보안 프레임워크로서 Spring Security의 핵심 기능입니다!

1. 빠른 시작

먼저 SpringBoot 프로젝트를 간단히 빌드해 보겠습니다.

이때 구축 성공 여부를 확인하기 위해 작성한 간단한 hello 인터페이스에 접근합니다.

그런 다음 SpringSecurity를 ​​소개합니다.

이번에는 접속 인터페이스의 효과를 살펴보겠습니다.

SpringSecurity가 도입된 후, 액세스 인터페이스는 자동으로 로그인 페이지로 이동합니다. 기본 사용자 이름은 user이며, 인터페이스에 액세스하려면 로그인해야 합니다.

2. 인증

2.1 로그인 인증 절차

먼저, 로그인 확인 프로세스를 이해해야 합니다. 먼저 프런트 엔드는 로그인 인터페이스에 액세스하기 위해 사용자 이름과 비밀번호를 전달합니다. 서버는 사용자 이름과 비밀번호를 얻은 후 이를 데이터베이스에 있는 것과 비교합니다. 사용자 이름/사용자 ID가 올바르게 사용되면 jwt가 생성되고 jwt를 프런트 엔드에 응답한 다음 로그인 후 다른 요청에 액세스하면 서버가 토큰을 얻을 때마다 요청 헤더에 토큰이 전달됩니다. 파싱을 위한 요청 헤더를 파싱하고, UserID를 획득하고, 사용자 이름 ID를 기반으로 사용자 관련 정보 및 뷰어 권한을 획득하고, 권한이 있는 경우 프런트엔드에 응답합니다.

2.2 원리의 예비 탐구

SpringSecurity의 원리는 실제로 다양한 기능을 가진 필터를 제공하는 필터 체인입니다. 여기서는 먼저 위의 빠른 시작과 관련된 필터를 살펴봅니다.

  • UsernamePasswordAuthenticationFilter는 로그인 페이지에 사용자 이름과 비밀번호를 입력한 후 로그인 요청을 처리하는 역할을 담당합니다.
  • ExceptionTranslationFilter는 필터 체인에서 발생한 모든 AccessDeniedException 및 AuthenticationException을 처리합니다.
  • FilterSecurityInterceptor는 권한 확인을 담당하는 필터입니다.

또한 디버그를 사용하여 현재 시스템의 SpringSecurity 필터 체인에 어떤 필터가 있는지와 그 순서를 확인할 수 있습니다.

다음으로 인증 흐름도 분석을 살펴보겠습니다.

여기서는 프로세스를 간단히 이해하면 됩니다.

사용자가 사용자 이름과 비밀번호를 제출하면 UsernamePasswordAuthenticationFilter는 이를 Authentication 객체로 캡슐화하고 인증을 위한 authenticate 메서드를 호출합니다. 그런 다음 인증을 위해 DaoAuthenticationProvider의 authenticate 메서드를 호출한 다음 loadUserByUserName 메서드를 호출하여 사용자에게 쿼리합니다. 메모리에서 검색한 다음 해당 사용자 정보를 UserDetails 개체에 캡슐화하고 PasswordEncoder를 사용하여 UserDetails의 비밀번호와 인증 비밀번호를 비교하여 올바른지 확인하는 것입니다. 올바른 경우 UserDetails의 권한 정보를 Authentication 개체에 설정합니다. 그런 다음 Authentication 객체를 반환하고 마지막으로 SecurityContextHolder.getContext()를 사용합니다. setAuthentication 메소드를 사용하여 이 객체를 저장하고 다른 필터는 SecurityContextHoder를 통해 현재 사용자 정보를 얻습니다. (이 문단을 이해하기 위해 외울 필요는 없습니다)

그런 다음 수정하기 전에 프로세스를 알고 있습니다. 먼저 메모리에서 검색할 때 데이터베이스에서 검색해야 하며(여기서는 UserDetailsService 구현 클래스를 사용자 정의해야 함) 기본 사용자 이름과 비밀번호를 사용하지 않습니다. , 로그인 인터페이스는 직접 작성해야 하며, 그가 제공한 기본 로그인 페이지를 사용할 필요가 없습니다.

우리가 분석한 상황을 바탕으로 이런 그림을 얻을 수 있다.

이때 jwt는 프론트 엔드로 반환되며 프론트 엔드에서 수행한 다른 요청은 토큰을 전달하므로 첫 번째 단계는 토큰이 전달되었는지 확인하고 토큰을 구문 분석하고 해당 사용자 ID를 얻은 후 캡슐화하는 것입니다. Anthentication 객체는 SecurityContextHolder에 저장됩니다(다른 필터가 이를 얻을 수 있도록).

그렇다면 여기에 또 다른 질문이 있습니다. jwt 인증 필터에서 사용자 ID를 가져온 후 완전한 사용자 정보를 얻는 방법은 무엇입니까?

여기서는 서버가 사용자 ID를 사용하여 프런트엔드에 jwt를 생성하여 인증할 때 사용자 ID가 키로 사용되고 사용자 정보가 값으로 Redis에 저장됩니다. redis에서 userid를 통해.

2.3 문제 해결

2.3.1 아이디어 분석

위의 원칙에 대한 사전 탐색을 통해 프런트엔드와 백엔드 분리 인증 프로세스를 직접 구현하는 경우 수행해야 할 작업을 대략적으로 분석했습니다.

로그인:

a. 사용자 정의 로그인 인터페이스

인증을 위해 ProviderManager 메소드를 호출합니다. 인증이 통과되면 jwt가 생성됩니다.

Redis에 사용자 정보 저장

b. UserDetailsService 사용자 정의

이 구현 클래스에서 데이터베이스를 쿼리합니다.

확인하다:

a. jwt 인증 필터를 사용자 정의합니다.

토큰 받기

토큰을 구문 분석하여 사용자 ID를 얻습니다.

Redis에서 완전한 사용자 정보 얻기

SecurityContextHolder에 저장

2.3.2 준비

먼저 해당 종속성을 추가해야 합니다.

  1. <!-- SpringSecurity启动器 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-security</artifactId>
  5. </dependency>
  6. <!-- redis依赖 -->
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-data-redis</artifactId>
  10. </dependency>
  11. <!-- fastjson依赖 -->
  12. <dependency>
  13. <groupId>com.alibaba</groupId>
  14. <artifactId>fastjson</artifactId>
  15. <version>1.2.33</version>
  16. </dependency>
  17. <!-- jwt依赖 -->
  18. <dependency>
  19. <groupId>io.jsonwebtoken</groupId>
  20. <artifactId>jjwt</artifactId>
  21. <version>0.9.0</version>
  22. </dependency>

그런 다음 Redis를 사용하고 Redis 관련 구성을 추가해야 합니다.

첫 번째는 FastJson의 직렬 변환기입니다.

  1. package org.example.utils;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.parser.ParserConfig;
  4. import com.alibaba.fastjson.serializer.SerializerFeature;
  5. import com.fasterxml.jackson.databind.JavaType;
  6. import com.fasterxml.jackson.databind.type.TypeFactory;
  7. import org.springframework.data.redis.serializer.RedisSerializer;
  8. import org.springframework.data.redis.serializer.SerializationException;
  9. import java.nio.charset.Charset;
  10. /**
  11. * Redis使用fastjson序列化
  12. * @param <T>
  13. */
  14. public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
  15. public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
  16. private Class<T> clazz;
  17. static {
  18. ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
  19. }
  20. public FastJsonRedisSerializer(Class<T> clazz){
  21. super();
  22. this.clazz=clazz;
  23. }
  24. @Override
  25. public byte[] serialize(T t) throws SerializationException {
  26. if (t == null){
  27. return new byte[0];
  28. }
  29. return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
  30. }
  31. @Override
  32. public T deserialize(byte[] bytes) throws SerializationException {
  33. if (bytes==null || bytes.length<=0){
  34. return null;
  35. }
  36. String str = new String(bytes,DEFAULT_CHARSET);
  37. return JSON.parseObject(str,clazz);
  38. }
  39. protected JavaType getJavaType(Class<?> clazz){
  40. return TypeFactory.defaultInstance().constructType(clazz);
  41. }
  42. }

RedisConfig를 생성하고 그 안에 직렬 변환기를 생성하여 문자 깨짐과 같은 문제를 해결합니다.

  1. package org.example.config;
  2. import org.example.utils.FastJsonRedisSerializer;
  3. import org.springframework.context.annotation.Bean;
  4. import org.springframework.context.annotation.Configuration;
  5. import org.springframework.data.redis.connection.RedisConnectionFactory;
  6. import org.springframework.data.redis.core.RedisTemplate;
  7. import org.springframework.data.redis.serializer.StringRedisSerializer;
  8. @Configuration
  9. public class RedisConfig {
  10. @Bean
  11. @SuppressWarnings(value = {"unchecked","rawtypes"})
  12. public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
  13. RedisTemplate<Object,Object> template = new RedisTemplate<>();
  14. template.setConnectionFactory(connectionFactory);
  15. FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
  16. //使用StringRedisSerializer来序列化和反序列化redus的key值
  17. template.setKeySerializer(new StringRedisSerializer());
  18. template.setValueSerializer(serializer);
  19. template.afterPropertiesSet();
  20. return template;
  21. }
  22. }

응답 클래스를 통일하는 것도 필요합니다

  1. package org.example.domain;
  2. import com.fasterxml.jackson.annotation.JsonInclude;
  3. @JsonInclude(JsonInclude.Include.NON_NULL)
  4. public class ResponseResult<T>{
  5. /**
  6. * 状态码
  7. */
  8. private Integer code;
  9. /**
  10. * 提示信息,如果有错误时,前端可以获取该字段进行提示
  11. */
  12. private String msg;
  13. /**
  14. * 查询到的结果数据
  15. */
  16. private T data;
  17. public ResponseResult(Integer code,String msg){
  18. this.code = code;
  19. this.msg = msg;
  20. }
  21. public ResponseResult(Integer code,T data){
  22. this.code = code;
  23. this.data = data;
  24. }
  25. public Integer getCode() {
  26. return code;
  27. }
  28. public void setCode(Integer code) {
  29. this.code = code;
  30. }
  31. public String getMsg() {
  32. return msg;
  33. }
  34. public void setMsg(String msg) {
  35. this.msg = msg;
  36. }
  37. public T getData() {
  38. return data;
  39. }
  40. public void setData(T data) {
  41. this.data = data;
  42. }
  43. public ResponseResult(Integer code,String msg,T data){
  44. this.code = code;
  45. this.msg = msg;
  46. this.data = data;
  47. }
  48. }

jwt를 생성하고 jwt를 구문 분석하려면 jwt 도구 클래스가 필요합니다.

  1. package org.example.utils;
  2. import io.jsonwebtoken.Claims;
  3. import io.jsonwebtoken.JwtBuilder;
  4. import io.jsonwebtoken.Jwts;
  5. import io.jsonwebtoken.SignatureAlgorithm;
  6. import javax.crypto.SecretKey;
  7. import javax.crypto.spec.SecretKeySpec;
  8. import java.util.Base64;
  9. import java.util.Date;
  10. import java.util.UUID;
  11. public class JwtUtil {
  12. //有效期为
  13. public static final Long JWT_TTL = 60*60*1000L; //一个小时
  14. //设置密钥明文
  15. public static final String JWT_KEY = "hzj";
  16. public static String getUUID(){
  17. String token = UUID.randomUUID().toString().replaceAll("-","");
  18. return token;
  19. }
  20. /**
  21. * 生成jwt
  22. * @param subject token中要存放的数据(json格式)
  23. * @return
  24. */
  25. public static String createJWT(String subject){
  26. JwtBuilder builder = getJwtBuilder(subject,null,getUUID()); //设置过期时间
  27. return builder.compact();
  28. }
  29. /**
  30. * 生成jwt
  31. * @param subject token中要存放的数据(json格式)
  32. * @param ttlMillis token超时时间
  33. * @return
  34. */
  35. public static String createJWT(String subject,Long ttlMillis){
  36. JwtBuilder builder = getJwtBuilder(subject,ttlMillis,getUUID()); //设置过期时间
  37. return builder.compact();
  38. }
  39. private static JwtBuilder getJwtBuilder(String subject,Long ttlMillis,String uuid){
  40. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
  41. SecretKey secretKey = generalkey();
  42. long nowMillis = System.currentTimeMillis();
  43. Date now = new Date(nowMillis);
  44. if(ttlMillis==null){
  45. ttlMillis=JwtUtil.JWT_TTL;
  46. }
  47. long expMillis = nowMillis + ttlMillis;
  48. Date expDate = new Date(expMillis);
  49. return Jwts.builder()
  50. .setId(uuid) //唯一的Id
  51. .setSubject(subject) //主题 可以是Json数据
  52. .setIssuer("hzj") //签发者
  53. .setIssuedAt(now) //签发时间
  54. .signWith(signatureAlgorithm,secretKey) //使用HS256对称加密算法签名,第二个参数为密钥
  55. .setExpiration(expDate);
  56. }
  57. /**
  58. * 创建token
  59. * @param id
  60. * @param subject
  61. * @param ttlMillis
  62. * @return
  63. */
  64. public static String createJWT(String id,String subject,Long ttlMillis){
  65. JwtBuilder builder = getJwtBuilder(subject,ttlMillis,id);//设置过期时间
  66. return builder.compact();
  67. }
  68. public static void main(String[] args) throws Exception{
  69. String token =
  70. "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTg0MjU5MzIsInVzZX" +
  71. "JJZCI6MTExLCJ1c2VybmFtZSI6Ik1hcmtaUVAifQ.PTlOdRG7ROVJqPrA0q2ac7rKFzNNFR3lTMyP_8fIw9Q";
  72. Claims claims = parseJWT(token);
  73. System.out.println(claims);
  74. }
  75. /**
  76. * 生成加密后的密钥secretkey
  77. * @return
  78. */
  79. public static SecretKey generalkey(){
  80. byte[] encodeedkey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
  81. SecretKey key = new SecretKeySpec(encodeedkey,0,encodeedkey.length,"AES");
  82. return key;
  83. }
  84. /**
  85. * 解析
  86. * @param jwt
  87. * @return
  88. * @throws Exception
  89. */
  90. public static Claims parseJWT(String jwt) throws Exception{
  91. SecretKey secretKey = generalkey();
  92. return Jwts.parser()
  93. .setSigningKey(secretKey)
  94. .parseClaimsJws(jwt)
  95. .getBody();
  96. }
  97. }

redistemplate을 더 쉽게 호출할 수 있도록 하는 또 다른 Redis 도구 클래스 RedisCache를 정의합니다.

  1. package org.example.utils;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.redis.core.BoundSetOperations;
  4. import org.springframework.data.redis.core.HashOperations;
  5. import org.springframework.data.redis.core.RedisTemplate;
  6. import org.springframework.data.redis.core.ValueOperations;
  7. import org.springframework.stereotype.Component;
  8. import java.util.*;
  9. import java.util.concurrent.TimeUnit;
  10. @SuppressWarnings(value = { "unchecked", "rawtypes" })
  11. @Component
  12. public class RedisCache
  13. {
  14. @Autowired
  15. public RedisTemplate redisTemplate;
  16. /**
  17. * 缓存基本的对象,Integer、String、实体类等
  18. *
  19. * @param key 缓存的键值
  20. * @param value 缓存的值
  21. */
  22. public <T> void setCacheObject(final String key, final T value)
  23. {
  24. redisTemplate.opsForValue().set(key, value);
  25. }
  26. /**
  27. * 缓存基本的对象,Integer、String、实体类等
  28. *
  29. * @param key 缓存的键值
  30. * @param value 缓存的值
  31. * @param timeout 时间
  32. * @param timeUnit 时间颗粒度
  33. */
  34. public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
  35. {
  36. redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
  37. }
  38. /**
  39. * 设置有效时间
  40. *
  41. * @param key Redis键
  42. * @param timeout 超时时间
  43. * @return true=设置成功;false=设置失败
  44. */
  45. public boolean expire(final String key, final long timeout)
  46. {
  47. return expire(key, timeout, TimeUnit.SECONDS);
  48. }
  49. /**
  50. * 设置有效时间
  51. *
  52. * @param key Redis键
  53. * @param timeout 超时时间
  54. * @param unit 时间单位
  55. * @return true=设置成功;false=设置失败
  56. */
  57. public boolean expire(final String key, final long timeout, final TimeUnit unit)
  58. {
  59. return redisTemplate.expire(key, timeout, unit);
  60. }
  61. /**
  62. * 获得缓存的基本对象。
  63. *
  64. * @param key 缓存键值
  65. * @return 缓存键值对应的数据
  66. */
  67. public <T> T getCacheObject(final String key)
  68. {
  69. ValueOperations<String, T> operation = redisTemplate.opsForValue();
  70. return operation.get(key);
  71. }
  72. /**
  73. * 删除单个对象
  74. *
  75. * @param key
  76. */
  77. public boolean deleteObject(final String key)
  78. {
  79. return redisTemplate.delete(key);
  80. }
  81. /**
  82. * 删除集合对象
  83. *
  84. * @param collection 多个对象
  85. * @return
  86. */
  87. public long deleteObject(final Collection collection)
  88. {
  89. return redisTemplate.delete(collection);
  90. }
  91. /**
  92. * 缓存List数据
  93. *
  94. * @param key 缓存的键值
  95. * @param dataList 待缓存的List数据
  96. * @return 缓存的对象
  97. */
  98. public <T> long setCacheList(final String key, final List<T> dataList)
  99. {
  100. Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
  101. return count == null ? 0 : count;
  102. }
  103. /**
  104. * 获得缓存的list对象
  105. *
  106. * @param key 缓存的键值
  107. * @return 缓存键值对应的数据
  108. */
  109. public <T> List<T> getCacheList(final String key)
  110. {
  111. return redisTemplate.opsForList().range(key, 0, -1);
  112. }
  113. /**
  114. * 缓存Set
  115. *
  116. * @param key 缓存键值
  117. * @param dataSet 缓存的数据
  118. * @return 缓存数据的对象
  119. */
  120. public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
  121. {
  122. BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
  123. Iterator<T> it = dataSet.iterator();
  124. while (it.hasNext())
  125. {
  126. setOperation.add(it.next());
  127. }
  128. return setOperation;
  129. }
  130. /**
  131. * 获得缓存的set
  132. *
  133. * @param key
  134. * @return
  135. */
  136. public <T> Set<T> getCacheSet(final String key)
  137. {
  138. return redisTemplate.opsForSet().members(key);
  139. }
  140. /**
  141. * 缓存Map
  142. *
  143. * @param key
  144. * @param dataMap
  145. */
  146. public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
  147. {
  148. if (dataMap != null) {
  149. redisTemplate.opsForHash().putAll(key, dataMap);
  150. }
  151. }
  152. /**
  153. * 获得缓存的Map
  154. *
  155. * @param key
  156. * @return
  157. */
  158. public <T> Map<String, T> getCacheMap(final String key)
  159. {
  160. return redisTemplate.opsForHash().entries(key);
  161. }
  162. /**
  163. * 往Hash中存入数据
  164. *
  165. * @param key Redis键
  166. * @param hKey Hash键
  167. * @param value 值
  168. */
  169. public <T> void setCacheMapValue(final String key, final String hKey, final T value)
  170. {
  171. redisTemplate.opsForHash().put(key, hKey, value);
  172. }
  173. /**
  174. * 获取Hash中的数据
  175. *
  176. * @param key Redis键
  177. * @param hKey Hash键
  178. * @return Hash中的对象
  179. */
  180. public <T> T getCacheMapValue(final String key, final String hKey)
  181. {
  182. HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
  183. return opsForHash.get(key, hKey);
  184. }
  185. /**
  186. * 删除Hash中的数据
  187. *
  188. * @param key
  189. * @param hkey
  190. */
  191. public void delCacheMapValue(final String key, final String hkey)
  192. {
  193. HashOperations hashOperations = redisTemplate.opsForHash();
  194. hashOperations.delete(key, hkey);
  195. }
  196. /**
  197. * 获取多个Hash中的数据
  198. *
  199. * @param key Redis键
  200. * @param hKeys Hash键集合
  201. * @return Hash对象集合
  202. */
  203. public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
  204. {
  205. return redisTemplate.opsForHash().multiGet(key, hKeys);
  206. }
  207. /**
  208. * 获得缓存的基本对象列表
  209. *
  210. * @param pattern 字符串前缀
  211. * @return 对象列表
  212. */
  213. public Collection<String> keys(final String pattern)
  214. {
  215. return redisTemplate.keys(pattern);
  216. }
  217. }

응답에 데이터를 쓸 수도 있으므로 WebUtils 도구 클래스도 필요합니다.

  1. package org.example.utils;
  2. import javax.servlet.http.HttpServletResponse;
  3. import java.io.IOException;
  4. public class WebUtils {
  5. /**
  6. * 将字符串渲染到客户端
  7. *
  8. * @param response 渲染对象
  9. * @param string 待渲染的字符串
  10. * @return null
  11. */
  12. public static String renderString(HttpServletResponse response, String string) {
  13. try
  14. {
  15. response.setStatus(200);
  16. response.setContentType("application/json");
  17. response.setCharacterEncoding("utf-8");
  18. response.getWriter().print(string);
  19. }
  20. catch (IOException e)
  21. {
  22. e.printStackTrace();
  23. }
  24. return null;
  25. }
  26. }

마지막으로 해당 사용자 엔터티 클래스를 작성합니다.

  1. package org.example.domain;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. import java.io.Serializable;
  6. import java.util.Date;
  7. /**
  8. * 用户表(User)实体类
  9. */
  10. @Data
  11. @AllArgsConstructor
  12. @NoArgsConstructor
  13. public class User implements Serializable {
  14. private static final long serialVersionUID = -40356785423868312L;
  15. /**
  16. * 主键
  17. */
  18. private Long id;
  19. /**
  20. * 用户名
  21. */
  22. private String userName;
  23. /**
  24. * 昵称
  25. */
  26. private String nickName;
  27. /**
  28. * 密码
  29. */
  30. private String password;
  31. /**
  32. * 账号状态(0正常 1停用)
  33. */
  34. private String status;
  35. /**
  36. * 邮箱
  37. */
  38. private String email;
  39. /**
  40. * 手机号
  41. */
  42. private String phonenumber;
  43. /**
  44. * 用户性别(0男,1女,2未知)
  45. */
  46. private String sex;
  47. /**
  48. * 头像
  49. */
  50. private String avatar;
  51. /**
  52. * 用户类型(0管理员,1普通用户)
  53. */
  54. private String userType;
  55. /**
  56. * 创建人的用户id
  57. */
  58. private Long createBy;
  59. /**
  60. * 创建时间
  61. */
  62. private Date createTime;
  63. /**
  64. * 更新人
  65. */
  66. private Long updateBy;
  67. /**
  68. * 更新时间
  69. */
  70. private Date updateTime;
  71. /**
  72. * 删除标志(0代表未删除,1代表已删除)
  73. */
  74. private Integer delFlag;
  75. }

위의 분석에 따르면 SpringSecuriry가 UserDetailsService를 사용할 수 있도록 UserDetailsService를 사용자 정의해야 합니다. 자체 UserDetailsService는 데이터베이스에서 사용자 이름과 비밀번호를 쿼리할 수 있습니다.

먼저 데이터베이스 테이블 sys_user를 생성합니다.

  1. CREATE TABLE `sys_user` (
  2. `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  3. `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  4. `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '呢称',
  5. `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
  6. `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常1停用)',
  7. `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
  8. `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
  9. `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
  10. `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
  11. `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(O管理员,1普通用户)',
  12. `create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
  13. `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  14. `update_by` bigint DEFAULT NULL COMMENT '更新人',
  15. `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  16. `del_flag` int DEFAULT '0' COMMENT '删除标志(O代表未删除,1代表已删除)',
  17. PRIMARY KEY (`id`)
  18. ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

그런 다음 myBatisPlus 및 mysql 드라이버를 소개합니다.

  1. <dependency>
  2. <groupId>com.baomidou</groupId>
  3. <artifactId>mybatis-plus-boot-starter</artifactId>
  4. <version>3.4.3</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>mysql</groupId>
  8. <artifactId>mysql-connector-java</artifactId>
  9. </dependency>

그런 다음 데이터베이스의 관련 정보를 구성합니다.

그런 다음 매퍼 인터페이스 UserMapper를 정의하고 mybatisplus를 사용하여 해당 주석을 추가합니다.

  1. package org.example.mapper;
  2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  3. import org.example.domain.User;
  4. public interface UserMapper extends BaseMapper<User> {
  5. }

그런 다음 구성 요소 검색을 구성합니다.

마지막으로 mp를 정상적으로 사용할 수 있는지 테스트해 보세요.

junit 소개

이렇게 하면 정상적으로 사용할 수 있습니다.

2.3.3 구현

2.3.3.1 데이터베이스 검증 사용자

다음으로 핵심 코드를 구현해야 합니다.

먼저 UserDetailsService를 사용자 정의해 보겠습니다.

  1. package org.example.service.impl;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import org.example.domain.LoginUser;
  4. import org.example.domain.User;
  5. import org.example.mapper.UserMapper;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.security.core.userdetails.UserDetails;
  8. import org.springframework.security.core.userdetails.UserDetailsService;
  9. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  10. import org.springframework.stereotype.Service;
  11. import java.util.Objects;
  12. @Service
  13. public class UserDetailsServiceImpl implements UserDetailsService {
  14. @Autowired
  15. private UserMapper userMapper;
  16. @Override
  17. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  18. //查询用户信息 [InMemoryUserDetailsManager是在内存中查找]
  19. LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
  20. wrapper.eq(User::getUserName,username);
  21. User user = userMapper.selectOne(wrapper);
  22. //如果查询不到数据就抛出异常,给出提示
  23. if(Objects.isNull(user)){
  24. throw new RuntimeException("用户名或密码错误!");
  25. }
  26. //TODO 查询权限信息
  27. //封装为UserDetails对象返回
  28. return new LoginUser(user);
  29. }
  30. }

여기서 사용자는 UserDetails로 캡슐화되어 반환됩니다.

  1. package org.example.domain;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.NoArgsConstructor;
  5. import org.springframework.security.core.GrantedAuthority;
  6. import org.springframework.security.core.userdetails.UserDetails;
  7. import java.util.Collection;
  8. @Data
  9. @AllArgsConstructor
  10. @NoArgsConstructor
  11. public class LoginUser implements UserDetails {
  12. private User user;
  13. @Override
  14. public Collection<? extends GrantedAuthority> getAuthorities() {
  15. return null;
  16. }
  17. @Override
  18. public String getPassword() {
  19. return user.getPassword();
  20. }
  21. @Override
  22. public String getUsername() {
  23. return user.getUserName();
  24. }
  25. @Override
  26. public boolean isAccountNonExpired() {
  27. return true;
  28. }
  29. @Override
  30. public boolean isAccountNonLocked() {
  31. return true;
  32. }
  33. @Override
  34. public boolean isCredentialsNonExpired() {
  35. return true;
  36. }
  37. @Override
  38. public boolean isEnabled() {
  39. return true;
  40. }
  41. }

마지막으로 여기에는 데이터베이스에서 데이터를 가져오기 위해 로그인 테스트를 수행해야 하며, 사용자의 비밀번호를 일반 텍스트로 전송하려면 테이블에 사용자 데이터를 써야 한다는 점이 있습니다. , 비밀번호 앞에 {noop}를 추가해야 합니다.

여기에 데이터베이스의 사용자 이름과 비밀번호를 입력하여 로그인할 수 있습니다.

2.3.3.2 비밀번호 암호화 저장

기본 PasswordEncoder에서는 데이터베이스의 비밀번호 형식이 {id}password가 되도록 요구하기 때문에 비밀번호 앞에 {noop}을 추가하는 이유에 대해 이야기해 보겠습니다. 이는 id를 기반으로 비밀번호의 암호화 방법을 결정하지만 일반적으로 우리는 이 방법을 채택하지 않으므로 PasswordEncoder를 교체해야 합니다.

다음으로 우리는 그것을 테스트하고 볼 것입니다.

여기에 전달한 두 개의 원래 비밀번호는 동일하지만 다른 결과를 얻었습니다. 이는 실제로 솔팅 알고리즘과 관련이 있습니다. 나중에 사용자 정의 암호화에 대한 기사도 작성할 것입니다.

암호화된 비밀번호를 얻은 후, 암호화된 비밀번호를 데이터베이스에 저장한 다음 프런트 엔드에서 전달된 일반 텍스트 비밀번호를 데이터베이스의 암호화된 비밀번호로 확인하여 로그인할 수 있습니다.

이때 로그인을 위해 프로젝트를 시작했는데 더 이상 이전 비밀번호로 로그인할 수 없다는 사실을 발견했습니다. 데이터베이스는 원래 비밀번호가 아닌 등록 단계에서 데이터베이스에 저장된 암호화된 비밀번호를 저장해야 하기 때문입니다. 등록하지 않았으므로 비밀번호를 암호화하겠습니다) 비밀번호는 데이터베이스 자체에 기록됩니다.

2.3.3.3 로그인 인터페이스

로그인 인터페이스를 구현한 후 SpringSecurity가 이를 허용하도록 해야 합니다. 허용되지 않으면 모순이 됩니다. 인터페이스에서는 AuthenticationManager의 authenticate 메소드를 통해 수행되므로 AuthenticationManager가 주입되도록 구성해야 합니다. SecurityConfig의 컨테이너에 추가합니다.

인증에 성공하면 사용자가 다음 요청 시 jwt를 통해 특정 사용자를 식별할 수 있도록 jwt를 생성하여 응답에 넣어야 하며 사용자 정보는 redis에 저장되어야 합니다. ID를 열쇠로 사용할 수 있습니다.

LoginController를 먼저 작성하세요.

그런 다음 해당 서비스를 작성하십시오.

SecurityConfig에 AuthenticationManager를 삽입하고 로그인 인터페이스를 해제합니다.

서비스의 비즈니스 로직에서는 인증에 실패하면 사용자 정의 예외가 반환되지만, 인증에 성공하면 해당 정보를 어떻게 얻습니까?

여기서 우리는 획득한 객체를 디버그하고 볼 수 있습니다.

해당 필수 정보는 교장에서 얻을 수 있습니다.

그런 다음 코드를 완성하세요.

마지막으로 테스트해 보세요.

2.3.3.4 인증 필터

먼저 코드를 붙여넣겠습니다.

  1. @Component
  2. public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
  3. @Autowired
  4. private RedisCache redisCache;
  5. @Override
  6. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  7. //获取token
  8. String token = request.getHeader("token");
  9. if (!StringUtils.hasText(token)) {
  10. //放行
  11. filterChain.doFilter(request, response); //这里放行是因为还有后续的过滤器会给出对应的异常
  12. return; //token为空 不执行后续流程
  13. }
  14. //解析token
  15. String userid;
  16. try {
  17. Claims claims = JwtUtil.parseJWT(token);
  18. userid = claims.getSubject();
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. throw new RuntimeException("token非法!");
  22. }
  23. //从redis中获取用户信息
  24. String redisKey = "login:" + userid;
  25. LoginUser loginUser = redisCache.getCacheObject(redisKey);
  26. if (Objects.isNull(loginUser)){
  27. throw new RuntimeException("用户未登录!");
  28. }
  29. //将信息存入SecurityContextHolder(因为过滤器链后面的filter都是从中获取认证信息进行对应放行)
  30. //TODO 获取权限信息封装到Authentication中
  31. UsernamePasswordAuthenticationToken authenticationToken =
  32. new UsernamePasswordAuthenticationToken(loginUser,null,null);
  33. SecurityContextHolder.getContext().setAuthentication(authenticationToken);
  34. //放行
  35. filterChain.doFilter(request,response); //此时的放行是携带认证的,不同于上方token为空的放行
  36. }
  37. }

우선 여기서 토큰을 얻으려면 요청 헤더에서 해당 토큰을 가져온 후 비어 있는지 확인하고 비어 있으면 후속 프로세스를 거치지 않고 직접 릴리스합니다. 토큰을 구문 분석하고, 내부에서 userid를 가져온 다음 redis에서 해당 사용자 정보를 얻은 후 마지막으로 SecurityContextHolder에 저장합니다. 왜냐하면 후속 필터는 일일 인증 정보를 가져와서 마지막으로 수행해야 하기 때문입니다. 분석 작업.

주의가 필요한 또 다른 점은 SecurityContextHolder.getContext().setAuthentication()이 인증 개체를 전달해야 한다는 것입니다. 개체를 빌드할 때 세 번째 매개 변수가 인증 여부를 결정하는 핵심이기 때문입니다.

다음으로 이 필터를 구성해야 합니다.

그런 다음 사용자/로그인 인터페이스에 액세스하면 토큰이 포함된 응답 본문이 반환됩니다. hello 인터페이스에 다시 액세스하면 403이 됩니다. 토큰을 전달하지 않으므로 위 코드에 해당합니다. 토큰이 없으면 응답 본문이 해제되고 반환은 후속 프로세스를 실행하지 않습니다. (여기서 해제하는 것은 나중에 처리하기 위해 특별히 예외를 발생시키는 다른 필터가 있기 때문이며 반환은 응답을 통과하지 못하도록 하기 위한 것입니다. 프로세스)

이때 사용자/로그인으로 생성된 토큰을 hello 인터페이스의 요청 헤더에 넣으면 정상적으로 접근할 수 있다.

그러면 필터 세트의 목적이 달성되었습니다(토큰 획득, 토큰 구문 분석 및 SecurityContextHolder에 저장).

2.3.3.5 로그아웃

이 시점에서는 로그아웃하기가 더 쉽습니다. 나중에 액세스하기 위해 토큰을 가져올 때 Redis의 해당 사용자 정보는 현재 사용자 정의 필터에서 얻을 수 있습니다. 얻을 수 없다는 것은 로그인되어 있지 않다는 뜻입니다.

/user/logout 인터페이스에 액세스하기 위해 이 토큰을 가지고 있습니다.

그런 다음 로그아웃 기능이 구현됩니다.

이번 글은 스테이션B 3차 업데이트를 통해 알게된 내용입니다! ! !