Technologieaustausch

SpringSecurity Framework [Authentifizierung]

2024-07-12

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

Inhaltsverzeichnis

1. Schnellstart

2. Zertifizierung

2.1 Anmeldebestätigungsprozess

2.2 Vorläufige Untersuchung des Prinzips

2.3 Lösen Sie das Problem

2.3.1 Ideenanalyse

2.3.2 Vorbereitung

2.3.3 Umsetzung

2.3.3.1 Benutzer der Datenbanküberprüfung

2.3.3.2 Passwortverschlüsselter Speicher

2.3.3.3 Login-Schnittstelle

2.3.3.4 Authentifizierungsfilter

2.3.3.5 Abmelden


Spring Security ist ein Sicherheitsmanagement-Framework der Spring-Familie und bietet im Vergleich zu Shiro, einem anderen Sicherheits-Framework, umfangreichere Funktionen und umfangreichere Community-Ressourcen.

Im Allgemeinen wird Spring Security häufiger in Großprojekten und Shiro häufiger in kleinen Projekten verwendet, da Shiro im Vergleich zu Spring Security einfacher zu verwenden ist.

Allgemeine Webanwendungen müssenZertifizierungUndAutorisieren

  • Authentifizierung: Überprüfen Sie, ob der aktuelle Benutzer, der auf das System zugreift, der Systembenutzer ist, und bestätigen Sie, um welchen Benutzer es sich handelt.
  • Autorisierung: Stellen Sie nach der Authentifizierung fest, ob der aktuelle Benutzer berechtigt ist, einen Vorgang auszuführen

Authentifizierung und Autorisierung sind die Kernfunktionen von Spring Security als Sicherheitsframework!

1. Schnellstart

Lassen Sie uns zunächst einfach ein SpringBoot-Projekt erstellen.

Zu diesem Zeitpunkt greifen wir auf eine einfache Hallo-Schnittstelle zu, die wir geschrieben haben, um zu überprüfen, ob die Konstruktion erfolgreich ist.

Dann stellen Sie SpringSecurity vor.

Schauen wir uns zu diesem Zeitpunkt die Auswirkungen der Zugriffsschnittstelle an.

Nach der Einführung von SpringSecurity springt die Zugriffsoberfläche automatisch zu einer Anmeldeseite und das Kennwort wird an die Konsole ausgegeben. Sie müssen sich anmelden, um auf die Schnittstelle zuzugreifen.

2. Zertifizierung

2.1 Anmeldebestätigungsprozess

Zunächst müssen wir den Anmeldeüberprüfungsprozess verstehen. Zuerst übermittelt das Frontend den Benutzernamen und das Kennwort, um auf die Anmeldeschnittstelle zuzugreifen. Nachdem der Server den Benutzernamen und das Kennwort erhalten hat, vergleicht er diese mit denen in der Datenbank. Wenn der Benutzername/die Benutzer-ID korrekt verwendet wird, wird ein JWT generiert, und dann antwortet der JWT an das Front-End und greift dann nach der Anmeldung auf andere Anforderungen zu. Jedes Mal, wenn der Server das Token erhält, wird es im Anforderungsheader angezeigt Analysieren Sie den Anforderungsheader, rufen Sie die Benutzer-ID ab und erhalten Sie benutzerbezogene Informationen und Betrachterberechtigungen basierend auf der Benutzernamen-ID. Wenn Sie über die entsprechende Berechtigung verfügen, antworten Sie dem Front-End.

2.2 Vorläufige Untersuchung des Prinzips

Das Prinzip von SpringSecurity ist eigentlich eine Filterkette, die Filter mit verschiedenen Funktionen bereitstellt. Hier betrachten wir zunächst die beteiligten Filter im obigen Schnellstart.

  • UsernamePasswordAuthenticationFilter ist für die Verarbeitung von Anmeldeanfragen verantwortlich, nachdem der Benutzername und das Kennwort auf der Anmeldeseite eingegeben wurden.
  • ExceptionTranslationFilter behandelt alle in der Filterkette ausgelösten AccessDeniedException und AuthenticationException
  • FilterSecurityInterceptor ist ein Filter, der für die Berechtigungsüberprüfung verantwortlich ist

Wir können Debug auch verwenden, um zu sehen, welche Filter sich in der SpringSecurity-Filterkette im aktuellen System befinden und in welcher Reihenfolge.

Schauen wir uns als Nächstes die Analyse des Authentifizierungsflussdiagramms an.

Hier müssen wir nur in der Lage sein, den Prozess zu verstehen:

Der Benutzer hat den Benutzernamen und das Kennwort übermittelt, UsernamePasswordAuthenticationFilter kapselt es als Authentifizierungsobjekt und ruft die Authentifizierungsmethode von DaoAuthenticationProvider zur Authentifizierung auf und ruft dann die LoadUserByUserName-Methode auf, um den Benutzer abzufragen besteht darin, im Speicher zu suchen und dann die entsprechenden Benutzerinformationen in ein UserDetails-Objekt zu kapseln. Verwenden Sie PasswordEncoder, um das Kennwort in UserDetails und das Authentifizierungskennwort zu vergleichen, um festzustellen, ob es korrekt ist, und legen Sie die Berechtigungsinformationen in UserDetails für das Authentifizierungsobjekt fest Geben Sie dann das Authentifizierungsobjekt zurück und verwenden Sie schließlich SecurityContextHolder.getContext(). Die setAuthentication-Methode speichert dieses Objekt, und andere Filter erhalten die aktuellen Benutzerinformationen über SecurityContextHoder. (Sie müssen diesen Absatz nicht auswendig lernen, um ihn zu verstehen)

Dann kennen wir den Prozess, bevor wir ihn ändern können. Wenn wir aus dem Speicher suchen, müssen wir zunächst in der Datenbank suchen (hier müssen wir eine UserDetailsService-Implementierungsklasse anpassen) und verwenden nicht den Standardbenutzernamen und das Kennwort Die Anmeldeschnittstelle muss von Ihnen selbst geschrieben werden und es besteht keine Notwendigkeit, die von ihm bereitgestellte Standardanmeldeseite zu verwenden.

Basierend auf der von uns analysierten Situation können wir uns ein solches Bild machen.

Zu diesem Zeitpunkt wird ein JWT an das Front-End zurückgegeben, und andere vom Front-End gestellte Anforderungen übertragen das Token. Unser erster Schritt besteht also darin, zu prüfen, ob das Token übertragen wird, das Token zu analysieren, die entsprechende Benutzer-ID abzurufen und zu kapseln es als Das Anthentication-Objekt wird im SecurityContextHolder gespeichert (damit andere Filter es abrufen können).

Hier stellt sich also eine weitere Frage: Wie erhalte ich vollständige Benutzerinformationen, nachdem ich die Benutzer-ID vom JWT-Authentifizierungsfilter erhalten habe?

Hier verwenden wir Redis. Wenn der Server die Benutzer-ID zum Generieren von JWT authentifiziert, wird die Benutzer-ID als Schlüssel verwendet und die Benutzerinformationen werden in Redis als Wert gespeichert. Anschließend können die vollständigen Benutzerinformationen abgerufen werden von Redis über die Benutzer-ID.

2.3 Lösen Sie das Problem

2.3.1 Ideenanalyse

Aus der vorläufigen Untersuchung der oben genannten Prinzipien haben wir auch grob analysiert, was wir tun müssen, wenn wir den Authentifizierungsprozess der Front-End- und Back-End-Trennung selbst implementieren.

Anmeldung:

a. Benutzerdefinierte Anmeldeschnittstelle

Rufen Sie die ProviderManager-Methode zur Authentifizierung auf. Wenn die Authentifizierung erfolgreich ist, wird ein JWT generiert.

Speichern Sie Benutzerinformationen in Redis

b. Passen Sie UserDetailsService an

Fragen Sie die Datenbank in dieser Implementierungsklasse ab

überprüfen:

a. Passen Sie den JWT-Authentifizierungsfilter an

Bekomme Token

Analysieren Sie das Token, um seine Benutzer-ID zu erhalten

Erhalten Sie vollständige Benutzerinformationen von Redis

Im SecurityContextHolder speichern

2.3.2 Vorbereitung

Zuerst müssen Sie die entsprechenden Abhängigkeiten hinzufügen

  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>

Dann müssen wir Redis verwenden und Redis-bezogene Konfigurationen hinzufügen.

Erstens ist der Serialisierer von 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. }

Erstellen Sie RedisConfig und erstellen Sie darin einen Serializer, um Probleme wie verstümmelte Zeichen zu lösen.

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

Es ist auch notwendig, die Antwortklasse zu vereinheitlichen

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

Sie benötigen JWT-Toolklassen, um JWT zu generieren und JWT zu analysieren.

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

Definieren Sie eine weitere Redis-Toolklasse RedisCache, die es uns erleichtert, Redistemplate aufzurufen

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

Möglicherweise schreiben wir auch Daten in die Antwort, daher benötigen wir auch eine Toolklasse 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. }

Schreiben Sie abschließend die entsprechende Benutzerentitätsklasse

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

Gemäß unserer obigen Analyse müssen wir einen UserDetailsService anpassen, damit SpringSecuriry unseren UserDetailsService verwenden kann. Unser eigener UserDetailsService kann den Benutzernamen und das Passwort aus der Datenbank abfragen.

Wir erstellen zunächst eine Datenbanktabelle 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='用户表';

Stellen Sie dann myBatisPlus- und MySQL-Treiber vor.

  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>

Anschließend konfigurieren Sie die relevanten Informationen der Datenbank.

Definieren Sie dann die Mapper-Schnittstelle UserMapper und fügen Sie mit mybatisplus entsprechende Anmerkungen hinzu.

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

Konfigurieren Sie dann das Scannen der Komponenten

Testen Sie abschließend, ob der MP normal verwendet werden kann.

Junit vorstellen

Auf diese Weise kann es normal verwendet werden.

2.3.3 Umsetzung

2.3.3.1 Benutzer der Datenbanküberprüfung

Als nächstes müssen wir den Kerncode implementieren.

Passen wir zunächst UserDetailsService an.

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

Hier wird der Benutzer als UserDetails gekapselt und zurückgegeben.

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

Schließlich gibt es hier noch einen Punkt: Wir müssen einen Anmeldetest durchführen, um Daten aus der Datenbank abzurufen. Wir müssen Benutzerdaten in die Tabelle schreiben und das Kennwort des Benutzers im Klartext übertragen , müssen Sie {noop} vor dem Passwort hinzufügen.

Hier können Sie den Benutzernamen und das Passwort in die Datenbank eingeben, um sich anzumelden.

2.3.3.2 Passwortverschlüsselter Speicher

Lassen Sie uns darüber sprechen, warum {noop} vor dem Passwort hinzugefügt wird, da der Standard-PasswordEncoder erfordert, dass das Passwortformat in der Datenbank {id}password ist. Es bestimmt die Verschlüsselungsmethode des Passworts basierend auf der ID, aber wir verwenden es im Allgemeinen Übernehmen Sie diese Methode nicht, daher muss PasswordEncoder ersetzt werden.

Als nächstes werden wir es testen und sehen.

Sie können sehen, dass die beiden ursprünglichen Passwörter, die wir hier eingegeben haben, gleich sind, aber wir haben unterschiedliche Ergebnisse erhalten. Dies hängt tatsächlich mit dem Salting-Algorithmus zusammen. Ich werde später auch einen Artikel über benutzerdefinierte Verschlüsselung schreiben.

Nachdem Sie das verschlüsselte Passwort erhalten haben, können Sie das verschlüsselte Passwort in der Datenbank speichern. Anschließend können Sie sich anmelden, indem Sie das vom Frontend übergebene Klartext-Passwort mit dem verschlüsselten Passwort in der Datenbank überprüfen.

Zu diesem Zeitpunkt starteten wir das Anmeldeprojekt und stellten fest, dass wir uns nicht mehr mit dem vorherigen Kennwort anmelden konnten, da die Datenbank das während der Registrierungsphase in der Datenbank gespeicherte verschlüsselte Kennwort und nicht das ursprüngliche Kennwort speichern sollte (weil ich nicht registriert, ich werde das Passwort verschlüsseln) Das Passwort wird von selbst in die Datenbank geschrieben).

2.3.3.3 Login-Schnittstelle

Wir müssen eine Anmeldeschnittstelle implementieren und diese dann von SpringSecurity zulassen lassen. Wenn dies nicht zulässig ist, erfolgt die Benutzerauthentifizierung über die Authentifizierungsmethode von AuthenticationManager. Daher müssen wir den AuthenticationManager für die Injektion konfigurieren in den Container in SecurityConfig.

Wenn die Authentifizierung erfolgreich ist, muss ein JWT generiert und in die Antwort eingefügt werden. Damit der Benutzer den spezifischen Benutzer bei der nächsten Anfrage über das JWT identifizieren kann, müssen die Benutzerinformationen in Redis gespeichert werden Als Schlüssel kann die ID verwendet werden.

Schreiben Sie zuerst LoginController

Dann schreiben Sie den entsprechenden Service.

Fügen Sie AuthenticationManager in SecurityConfig ein und geben Sie die Anmeldeschnittstelle frei.

Wenn in der Geschäftslogik im Dienst die Authentifizierung fehlschlägt, wird eine benutzerdefinierte Ausnahme zurückgegeben. Wenn die Authentifizierung jedoch erfolgreich ist, wie erhalten wir die entsprechenden Informationen?

Hier können wir debuggen und die erhaltenen Objekte sehen.

Hier finden Sie, dass die entsprechenden erforderlichen Informationen im Auftraggeber eingeholt werden können.

Vervollständigen Sie dann den Code.

Testen Sie es abschließend.

2.3.3.4 Authentifizierungsfilter

Ich füge zuerst den Code ein.

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

Um das Token hier abzurufen, holen wir uns zunächst das entsprechende Token aus dem Anforderungsheader und prüfen dann, ob es leer ist. Wenn es leer ist, geben wir es direkt frei, ohne den Folgeprozess zu durchlaufen Analysiert das Token, ruft die darin enthaltene Benutzer-ID ab und verwendet dann die Benutzer-ID basierend auf dem. Rufen Sie die entsprechenden Benutzerinformationen von Redis ab und speichern Sie sie schließlich im SecurityContextHolder, da nachfolgende Filter die täglichen Authentifizierungsinformationen daraus abrufen und schließlich ausführen müssen Analyseoperationen.

Ein weiterer Punkt, der Aufmerksamkeit erfordert, ist, dass SecurityContextHolder.getContext().setAuthentication() das Authentifizierungsobjekt übergeben muss. Wenn wir das Objekt erstellen, verwenden wir drei Parameter, da der dritte Parameter der Schlüssel ist, um zu bestimmen, ob eine Authentifizierung durchgeführt werden soll oder nicht.

Als nächstes müssen wir diesen Filter konfigurieren.

Wenn wir dann auf die Benutzer-/Anmeldeschnittstelle zugreifen, wird uns ein Antworttext mit einem Token zurückgegeben. Wenn wir erneut auf die Hallo-Schnittstelle zugreifen, lautet dieser 403. Da er kein Token enthält, entspricht er dem obigen Code. Ohne Token wird der Antworttext freigegeben und die Rückgabe führt den nachfolgenden Prozess nicht aus (Die Freigabe erfolgt hier, weil es andere Filter gibt, die speziell Ausnahmen für die spätere Verarbeitung auslösen, und die Rückgabe soll verhindern, dass die Antwort durchlaufen wird Verfahren)

Wenn wir zu diesem Zeitpunkt das von Benutzer/Anmeldung generierte Token in den Anforderungsheader der Hallo-Schnittstelle einfügen, können wir normal darauf zugreifen.

Dann ist der Zweck unseres Filtersatzes erreicht (Token erhalten, Token analysieren und im SecurityContextHolder speichern).

2.3.3.5 Abmelden

Zu diesem Zeitpunkt ist es für uns einfacher, die entsprechenden Daten in Redis zu löschen. Wenn wir später auf das Token zugreifen, werden die entsprechenden Benutzerinformationen in unserem benutzerdefinierten Filter abgerufen Sie können es nicht abrufen. Das bedeutet, dass Sie nicht angemeldet sind.

Wir tragen dieses Token, um auf die /user/logout-Schnittstelle zuzugreifen.

Anschließend wird die Abmeldefunktion implementiert.

Dieser Artikel wurde aus dem dritten Update von Station B gelernt! ! !