技術共有

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 ファミリーのセキュリティ管理フレームワークであり、別のセキュリティ フレームワークである Roku と比較すると、Shiro よりも豊富な機能と豊富なコミュニティ リソースを提供します。

一般に、Spring Security は大規模なプロジェクトで使用されることが多く、Shiro は Spring Security に比べて始めやすいため、小規模なプロジェクトで使用されることが多くなります。

一般的な Web アプリケーションでは、認証そして承認する

  • 認証: システムにアクセスしている現在のユーザーがシステム ユーザーであるかどうかを検証し、どのユーザーであるかを確認します。
  • 認可: 認証後、現在のユーザーに操作を実行する権限があるかどうかを判断します。

認証と認可は、セキュリティ フレームワークとしての 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 を通じて現在のユーザー情報を取得します。 (この段落を理解するために暗記する必要はありません)

次に、変更する前にプロセスを把握します。まず、メモリから検索する場合はデータベースから検索する必要があり (ここでは UserDetailsS​​ervice 実装クラスをカスタマイズする必要があります)、デフォルトのユーザー名とパスワードは使用しません。 、ログイン インターフェイスは自分で作成する必要があり、彼が提供するデフォルトのログイン ページを使用する必要はありません。

私たちが分析した状況に基づいて、このようなイメージを得ることができます。

この時点で、jwt がフロントエンドに返され、フロントエンドによって行われた他のリクエストでトークンが送信されます。そのため、最初のステップは、トークンが送信されているかどうかを確認し、トークンを解析し、対応するユーザー ID を取得してカプセル化することです。 Anthentication オブジェクトとして SecurityContextHolder に保存されます (他のフィルターがそれを取得できるようにするため)。

ここで別の質問があります。jwt 認証フィルターからユーザー ID を取得した後、完全なユーザー情報を取得するにはどうすればよいですか?

ここでは redis を使用します。サーバーがユーザー ID を使用して認証を行い、フロントエンドに jwt を生成すると、ユーザー ID がキーとして使用され、ユーザーの情報が値として redis に保存されます。これにより、完全なユーザー情報が取得されます。 Redis からユーザー ID 経由で。

2.3 問題を解決する

2.3.1 アイデア分析

上記の原則の予備的な検討から、フロントエンドとバックエンドの分離の認証プロセスを独自に実装する場合に何をする必要があるかについても大まかに分析しました。

ログイン:

a. カスタムログインインターフェース

認証のために ProviderManager メソッドを呼び出します。認証が成功すると、jwt が生成されます。

ユーザー情報をredisに保存する

b. UserDetailsS​​ervice をカスタマイズする

この実装クラスでデータベースにクエリを実行します

チェック:

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. }

別の Redis ツール クラス RedisCache を定義します。これにより、redistemplate の呼び出しが容易になります。

  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. }

上記の分析によると、SpringSecurity が UserDetailsS​​ervice を使用できるように UserDetailsS​​ervice をカスタマイズする必要があります。独自の UserDetailsS​​ervice は、データベースからユーザー名とパスワードをクエリできます。

まず、データベース テーブル 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 データベース検証ユーザー

次に、コアコードを実装する必要があります。

まず、UserDetailsS​​ervice をカスタマイズしましょう。

  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 パスワード暗号化ストレージ

パスワードの前に {noop} が追加される理由について説明します。デフォルトの PasswordEncoder では、データベース内のパスワード形式が {id}password である必要がありますが、一般的には ID に基づいてパスワードの暗号化方式が決定されます。この方法は採用されていないため、PasswordEncoder を置き換える必要があります。

次にテストして確認してみます。

ここで渡した 2 つの元のパスワードは同じであることがわかりますが、これは実際にはソルティング アルゴリズムに関連しています。後でカスタム暗号化についても記事を書きます。

暗号化されたパスワードを取得したら、その暗号化されたパスワードをデータベースに保存し、フロントエンドから渡された平文のパスワードをデータベース内の暗号化されたパスワードと照合することでログインできます。

この時点で、ログインするプロジェクトを開始しましたが、以前のパスワードではログインできないことがわかりました。データベースには、元のパスワードではなく、登録フェーズ中にデータベースに保存された暗号化されたパスワードが保存される必要があるためです(登録しませんでした。パスワードを暗号化します) パスワードは自動的にデータベースに書き込まれます)。

2.3.3.3 ログインインターフェース

ログインインターフェースを実装して SpringSecurity に許可させる必要があります。これが許可されていない場合、インターフェースでは AuthenticationManager の認証メソッドを通じてユーザー認証が実行されるため、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. }

まず、ここでトークンを取得するために、リクエストヘッダーから対応するトークンを取得し、それが空であることを確認し、後続のプロセスを経ずに直接解放します。トークンを解析し、内部のユーザー ID を取得し、そのユーザー ID に基づいて対応するユーザー情報を Redis から取得し、最後に SecurityContextHolder に保存します。これは、後続のフィルターがそこから毎日の認証情報を取得し、最後に分析を実行する必要があるためです。オペレーション。

もう 1 つの注意が必要な点は、SecurityContextHolder.getContext().setAuthentication() が認証オブジェクトを渡す必要があることです。これは、3 番目のパラメーターが認証するかどうかを決定するキーであるためです。

次に、このフィルターを構成する必要があります。

その後、ユーザー/ログイン インターフェイスにアクセスすると、トークンを含む応答本文が返されます。hello インターフェイスに再度アクセスすると、403 になります。トークンが含まれていないため、上記のコードに対応します。トークンがないと、応答本文は解放され、リターンは後続のプロセスを実行しません (ここでの解放は、後で処理するために特別に例外をスローする他のフィルターがあるためであり、リターンは応答を通過しないようにするためです。プロセス)

このとき、user/loginが生成したトークンをhelloインターフェースのリクエストヘッダに入れると正常にアクセスできるようになります。

これで、一連のフィルターの目的が達成されました (トークンの取得、トークンの解析、およびそれらの SecurityContextHolder への保管)。

2.3.3.5 ログアウト

この時点で、redis 内の対応するデータを削除するだけで済みます。後でアクセスするために、redis 内の対応するユーザー情報がカスタム フィルターで取得されます。取得できない場合は、ログインしていないことを意味します。

/user/logout インターフェイスにアクセスするためにこのトークンを運びます。

次に、ログアウト機能が実装されます。

この記事はb局の第3回更新で知りました! ! !