2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Table des matières
2.1 Processus de vérification de connexion
2.2 Exploration préliminaire du principe
2.3.3.1 Utilisateur de vérification de la base de données
2.3.3.2 Stockage crypté par mot de passe
2.3.3.3 Interface de connexion
2.3.3.4 Filtre d'authentification
Spring Security est un framework de gestion de la sécurité de la famille Spring. Comparé à Shiro, un autre framework de sécurité, il offre des fonctions plus riches et des ressources communautaires plus riches que Shiro.
De manière générale, Spring Security est plus couramment utilisé dans les projets à grande échelle, et Shiro est plus couramment utilisé dans les petits projets, car comparé à Spring Security, Shiro est plus facile à démarrer.
Les applications Web générales doiventAttestationetAutoriser。
L'authentification et l'autorisation sont les fonctions essentielles de Spring Security en tant que cadre de sécurité !
Commençons par créer simplement un projet SpringBoot.
À ce moment-là, nous accédons à une simple interface hello que nous avons écrite pour vérifier si la construction est réussie.
Présentez ensuite SpringSecurity.
À ce stade, jetons un coup d’œil à l’effet de l’interface d’accès.
Une fois SpringSecurity introduit, l'interface d'accès passera automatiquement à une page de connexion. Le nom d'utilisateur par défaut est user et le mot de passe sera affiché sur la console. Vous devez vous connecter pour accéder à l'interface.
Tout d'abord, nous devons comprendre le processus de vérification de connexion. Tout d'abord, le frontal contient le nom d'utilisateur et le mot de passe pour accéder à l'interface de connexion. Une fois que le serveur a obtenu le nom d'utilisateur et le mot de passe, il les compare avec ceux de la base de données. Si le nom d'utilisateur/l'ID utilisateur est utilisé correctement, un jwt est généré, puis Répondre à jwt au front-end, puis accéder aux autres requêtes après la connexion portera le jeton dans l'en-tête de la requête à chaque fois que le serveur obtient le jeton. l'en-tête de la demande d'analyse, obtient l'ID utilisateur et obtient les informations relatives à l'utilisateur et les autorisations du spectateur en fonction de l'identifiant du nom d'utilisateur, si vous avez l'autorisation, répondez au frontal.
Le principe de SpringSecurity est en fait une chaîne de filtres, qui fournit aux filtres diverses fonctions. Ici, nous examinons d'abord les filtres impliqués dans le démarrage rapide ci-dessus.
Nous pouvons également utiliser Debug pour voir quels filtres se trouvent dans la chaîne de filtres SpringSecurity dans le système actuel et leur ordre.
Jetons ensuite un coup d’œil à l’analyse de l’organigramme d’authentification.
Ici, il suffit de pouvoir comprendre le processus. Pour faire simple :
L'utilisateur a soumis le nom d'utilisateur et le mot de passe, UsernamePasswordAuthenticationFilter l'encapsule en tant qu'objet d'authentification et appelle la méthode d'authentification pour l'authentification. Il appelle ensuite la méthode d'authentification de DaoAuthenticationProvider pour l'authentification, puis appelle la méthode loadUserByUserName pour interroger l'utilisateur. consiste à rechercher en mémoire, puis à encapsuler les informations utilisateur correspondantes dans un objet UserDetails, à utiliser PasswordEncoder pour comparer le mot de passe dans UserDetails et le mot de passe d'authentification pour voir s'il est correct, s'il est correct, définissez les informations d'autorisation dans UserDetails sur l'objet d'authentification. , puis renvoyez l'objet d'authentification et enfin utilisez la méthode SecurityContextHolder.getContext().setAuthentication pour stocker cet objet, et d'autres filtres obtiendront les informations sur l'utilisateur actuel via SecurityContextHoder. (Vous n'avez pas besoin de mémoriser ce paragraphe pour le comprendre)
Ensuite, nous connaissons le processus avant de pouvoir le modifier. Tout d'abord, lors d'une recherche dans la mémoire, nous devons effectuer une recherche dans la base de données (ici nous devons personnaliser une classe d'implémentation UserDetailsService), et nous n'utiliserons pas le nom d'utilisateur et le mot de passe par défaut. , et l'interface de connexion doit être écrite par vous-même, et il n'est pas nécessaire d'utiliser la page de connexion par défaut fournie par lui.
Sur la base de la situation que nous avons analysée, nous pouvons obtenir une telle image.
À ce stade, un jwt est renvoyé au front-end et les autres requêtes effectuées par le front-end porteront le jeton. Notre première étape consiste donc à vérifier si le jeton est transporté, à analyser le jeton, à obtenir l'ID utilisateur correspondant et à l'encapsuler. L'objet Anthentication est stocké dans SecurityContextHolder (afin que d'autres filtres puissent l'obtenir).
Il y a donc une autre question ici. Comment obtenir des informations complètes sur l'utilisateur après avoir obtenu l'ID utilisateur du filtre d'authentification jwt ?
Ici, nous utilisons redis. Lorsque le serveur s'authentifie en utilisant l'identifiant utilisateur pour générer jwt sur le front-end, l'identifiant utilisateur est utilisé comme clé et les informations de l'utilisateur sont stockées dans redis comme valeur. Les informations utilisateur complètes peuvent ensuite être obtenues. de Redis via l'ID utilisateur.
À partir de l'exploration préliminaire des principes ci-dessus, nous avons également analysé grossièrement ce que nous devons faire si nous mettons en œuvre nous-mêmes le processus d'authentification de séparation front-end et back-end.
Se connecter:
a. Interface de connexion personnalisée
Appelez la méthode ProviderManager pour l'authentification. Si l'authentification réussit, un jwt est généré.
Stocker les informations utilisateur dans Redis
b. Personnaliser le service UserDetails
Interroger la base de données dans cette classe d'implémentation
vérifier:
a. Personnaliser le filtre d'authentification jwt
Obtenir un jeton
Analyser le jeton pour obtenir son identifiant utilisateur
Obtenez des informations utilisateur complètes à partir de Redis
Stocker dans SecurityContextHolder
Vous devez d'abord ajouter les dépendances correspondantes
- <!-- SpringSecurity启动器 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
-
- <!-- redis依赖 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- <!-- fastjson依赖 -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.33</version>
- </dependency>
- <!-- jwt依赖 -->
- <dependency>
- <groupId>io.jsonwebtoken</groupId>
- <artifactId>jjwt</artifactId>
- <version>0.9.0</version>
- </dependency>
Ensuite, nous devons utiliser Redis et ajouter les configurations liées à Redis.
Le premier est le sérialiseur de FastJson
- package org.example.utils;
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.parser.ParserConfig;
- import com.alibaba.fastjson.serializer.SerializerFeature;
- import com.fasterxml.jackson.databind.JavaType;
- import com.fasterxml.jackson.databind.type.TypeFactory;
- import org.springframework.data.redis.serializer.RedisSerializer;
- import org.springframework.data.redis.serializer.SerializationException;
-
- import java.nio.charset.Charset;
-
- /**
- * Redis使用fastjson序列化
- * @param <T>
- */
- public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
-
- public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
- private Class<T> clazz;
-
- static {
- ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
- }
-
- public FastJsonRedisSerializer(Class<T> clazz){
- super();
- this.clazz=clazz;
- }
-
- @Override
- public byte[] serialize(T t) throws SerializationException {
- if (t == null){
- return new byte[0];
- }
- return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
- }
-
- @Override
- public T deserialize(byte[] bytes) throws SerializationException {
- if (bytes==null || bytes.length<=0){
- return null;
- }
- String str = new String(bytes,DEFAULT_CHARSET);
-
- return JSON.parseObject(str,clazz);
- }
-
- protected JavaType getJavaType(Class<?> clazz){
- return TypeFactory.defaultInstance().constructType(clazz);
- }
-
- }
Créez RedisConfig et créez-y un sérialiseur pour résoudre des problèmes tels que des caractères tronqués.
- package org.example.config;
- import org.example.utils.FastJsonRedisSerializer;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.connection.RedisConnectionFactory;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
-
- @Configuration
- public class RedisConfig {
- @Bean
- @SuppressWarnings(value = {"unchecked","rawtypes"})
- public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
- RedisTemplate<Object,Object> template = new RedisTemplate<>();
- template.setConnectionFactory(connectionFactory);
-
- FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
-
- //使用StringRedisSerializer来序列化和反序列化redus的key值
- template.setKeySerializer(new StringRedisSerializer());
- template.setValueSerializer(serializer);
-
- template.afterPropertiesSet();
- return template;
- }
- }
Il est également nécessaire d'unifier la classe de réponse
- package org.example.domain;
- import com.fasterxml.jackson.annotation.JsonInclude;
- @JsonInclude(JsonInclude.Include.NON_NULL)
- public class ResponseResult<T>{
- /**
- * 状态码
- */
- private Integer code;
- /**
- * 提示信息,如果有错误时,前端可以获取该字段进行提示
- */
- private String msg;
- /**
- * 查询到的结果数据
- */
- private T data;
-
- public ResponseResult(Integer code,String msg){
- this.code = code;
- this.msg = msg;
- }
-
- public ResponseResult(Integer code,T data){
- this.code = code;
- this.data = data;
- }
-
- public Integer getCode() {
- return code;
- }
-
- public void setCode(Integer code) {
- this.code = code;
- }
-
- public String getMsg() {
- return msg;
- }
-
- public void setMsg(String msg) {
- this.msg = msg;
- }
-
- public T getData() {
- return data;
- }
-
- public void setData(T data) {
- this.data = data;
- }
-
- public ResponseResult(Integer code,String msg,T data){
- this.code = code;
- this.msg = msg;
- this.data = data;
- }
- }
Vous avez besoin de classes d'outils jwt pour générer jwt et analyser jwt.
- package org.example.utils;
-
- import io.jsonwebtoken.Claims;
- import io.jsonwebtoken.JwtBuilder;
- import io.jsonwebtoken.Jwts;
- import io.jsonwebtoken.SignatureAlgorithm;
-
- import javax.crypto.SecretKey;
- import javax.crypto.spec.SecretKeySpec;
- import java.util.Base64;
- import java.util.Date;
- import java.util.UUID;
-
- public class JwtUtil {
-
- //有效期为
- public static final Long JWT_TTL = 60*60*1000L; //一个小时
- //设置密钥明文
- public static final String JWT_KEY = "hzj";
-
- public static String getUUID(){
- String token = UUID.randomUUID().toString().replaceAll("-","");
- return token;
- }
-
- /**
- * 生成jwt
- * @param subject token中要存放的数据(json格式)
- * @return
- */
- public static String createJWT(String subject){
- JwtBuilder builder = getJwtBuilder(subject,null,getUUID()); //设置过期时间
- return builder.compact();
- }
-
- /**
- * 生成jwt
- * @param subject token中要存放的数据(json格式)
- * @param ttlMillis token超时时间
- * @return
- */
- public static String createJWT(String subject,Long ttlMillis){
- JwtBuilder builder = getJwtBuilder(subject,ttlMillis,getUUID()); //设置过期时间
- return builder.compact();
- }
-
- private static JwtBuilder getJwtBuilder(String subject,Long ttlMillis,String uuid){
- SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
- SecretKey secretKey = generalkey();
- long nowMillis = System.currentTimeMillis();
- Date now = new Date(nowMillis);
- if(ttlMillis==null){
- ttlMillis=JwtUtil.JWT_TTL;
- }
- long expMillis = nowMillis + ttlMillis;
- Date expDate = new Date(expMillis);
- return Jwts.builder()
- .setId(uuid) //唯一的Id
- .setSubject(subject) //主题 可以是Json数据
- .setIssuer("hzj") //签发者
- .setIssuedAt(now) //签发时间
- .signWith(signatureAlgorithm,secretKey) //使用HS256对称加密算法签名,第二个参数为密钥
- .setExpiration(expDate);
- }
-
- /**
- * 创建token
- * @param id
- * @param subject
- * @param ttlMillis
- * @return
- */
- public static String createJWT(String id,String subject,Long ttlMillis){
- JwtBuilder builder = getJwtBuilder(subject,ttlMillis,id);//设置过期时间
- return builder.compact();
- }
-
- public static void main(String[] args) throws Exception{
- String token =
- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTg0MjU5MzIsInVzZX" +
- "JJZCI6MTExLCJ1c2VybmFtZSI6Ik1hcmtaUVAifQ.PTlOdRG7ROVJqPrA0q2ac7rKFzNNFR3lTMyP_8fIw9Q";
- Claims claims = parseJWT(token);
- System.out.println(claims);
- }
-
- /**
- * 生成加密后的密钥secretkey
- * @return
- */
- public static SecretKey generalkey(){
- byte[] encodeedkey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
- SecretKey key = new SecretKeySpec(encodeedkey,0,encodeedkey.length,"AES");
- return key;
- }
-
- /**
- * 解析
- * @param jwt
- * @return
- * @throws Exception
- */
- public static Claims parseJWT(String jwt) throws Exception{
- SecretKey secretKey = generalkey();
- return Jwts.parser()
- .setSigningKey(secretKey)
- .parseClaimsJws(jwt)
- .getBody();
- }
- }
Définir une autre classe d'outils Redis RedisCache, ce qui nous permet d'appeler plus facilement Redistemplate
- package org.example.utils;
-
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.BoundSetOperations;
- import org.springframework.data.redis.core.HashOperations;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.core.ValueOperations;
- import org.springframework.stereotype.Component;
-
- import java.util.*;
- import java.util.concurrent.TimeUnit;
-
- @SuppressWarnings(value = { "unchecked", "rawtypes" })
- @Component
- public class RedisCache
- {
- @Autowired
- public RedisTemplate redisTemplate;
-
- /**
- * 缓存基本的对象,Integer、String、实体类等
- *
- * @param key 缓存的键值
- * @param value 缓存的值
- */
- public <T> void setCacheObject(final String key, final T value)
- {
- redisTemplate.opsForValue().set(key, value);
- }
-
- /**
- * 缓存基本的对象,Integer、String、实体类等
- *
- * @param key 缓存的键值
- * @param value 缓存的值
- * @param timeout 时间
- * @param timeUnit 时间颗粒度
- */
- public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
- {
- redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
- }
-
- /**
- * 设置有效时间
- *
- * @param key Redis键
- * @param timeout 超时时间
- * @return true=设置成功;false=设置失败
- */
- public boolean expire(final String key, final long timeout)
- {
- return expire(key, timeout, TimeUnit.SECONDS);
- }
-
- /**
- * 设置有效时间
- *
- * @param key Redis键
- * @param timeout 超时时间
- * @param unit 时间单位
- * @return true=设置成功;false=设置失败
- */
- public boolean expire(final String key, final long timeout, final TimeUnit unit)
- {
- return redisTemplate.expire(key, timeout, unit);
- }
-
- /**
- * 获得缓存的基本对象。
- *
- * @param key 缓存键值
- * @return 缓存键值对应的数据
- */
- public <T> T getCacheObject(final String key)
- {
- ValueOperations<String, T> operation = redisTemplate.opsForValue();
- return operation.get(key);
- }
-
- /**
- * 删除单个对象
- *
- * @param key
- */
- public boolean deleteObject(final String key)
- {
- return redisTemplate.delete(key);
- }
-
- /**
- * 删除集合对象
- *
- * @param collection 多个对象
- * @return
- */
- public long deleteObject(final Collection collection)
- {
- return redisTemplate.delete(collection);
- }
-
- /**
- * 缓存List数据
- *
- * @param key 缓存的键值
- * @param dataList 待缓存的List数据
- * @return 缓存的对象
- */
- public <T> long setCacheList(final String key, final List<T> dataList)
- {
- Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
- return count == null ? 0 : count;
- }
-
- /**
- * 获得缓存的list对象
- *
- * @param key 缓存的键值
- * @return 缓存键值对应的数据
- */
- public <T> List<T> getCacheList(final String key)
- {
- return redisTemplate.opsForList().range(key, 0, -1);
- }
-
- /**
- * 缓存Set
- *
- * @param key 缓存键值
- * @param dataSet 缓存的数据
- * @return 缓存数据的对象
- */
- public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
- {
- BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
- Iterator<T> it = dataSet.iterator();
- while (it.hasNext())
- {
- setOperation.add(it.next());
- }
- return setOperation;
- }
-
- /**
- * 获得缓存的set
- *
- * @param key
- * @return
- */
- public <T> Set<T> getCacheSet(final String key)
- {
- return redisTemplate.opsForSet().members(key);
- }
-
- /**
- * 缓存Map
- *
- * @param key
- * @param dataMap
- */
- public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
- {
- if (dataMap != null) {
- redisTemplate.opsForHash().putAll(key, dataMap);
- }
- }
-
- /**
- * 获得缓存的Map
- *
- * @param key
- * @return
- */
- public <T> Map<String, T> getCacheMap(final String key)
- {
- return redisTemplate.opsForHash().entries(key);
- }
-
- /**
- * 往Hash中存入数据
- *
- * @param key Redis键
- * @param hKey Hash键
- * @param value 值
- */
- public <T> void setCacheMapValue(final String key, final String hKey, final T value)
- {
- redisTemplate.opsForHash().put(key, hKey, value);
- }
-
- /**
- * 获取Hash中的数据
- *
- * @param key Redis键
- * @param hKey Hash键
- * @return Hash中的对象
- */
- public <T> T getCacheMapValue(final String key, final String hKey)
- {
- HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
- return opsForHash.get(key, hKey);
- }
-
- /**
- * 删除Hash中的数据
- *
- * @param key
- * @param hkey
- */
- public void delCacheMapValue(final String key, final String hkey)
- {
- HashOperations hashOperations = redisTemplate.opsForHash();
- hashOperations.delete(key, hkey);
- }
-
- /**
- * 获取多个Hash中的数据
- *
- * @param key Redis键
- * @param hKeys Hash键集合
- * @return Hash对象集合
- */
- public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
- {
- return redisTemplate.opsForHash().multiGet(key, hKeys);
- }
-
- /**
- * 获得缓存的基本对象列表
- *
- * @param pattern 字符串前缀
- * @return 对象列表
- */
- public Collection<String> keys(final String pattern)
- {
- return redisTemplate.keys(pattern);
- }
- }
-
Nous pouvons également écrire des données dans la réponse, nous avons donc également besoin d'une classe d'outils WebUtils
- package org.example.utils;
-
- import javax.servlet.http.HttpServletResponse;
- import java.io.IOException;
-
- public class WebUtils {
- /**
- * 将字符串渲染到客户端
- *
- * @param response 渲染对象
- * @param string 待渲染的字符串
- * @return null
- */
- public static String renderString(HttpServletResponse response, String string) {
- try
- {
- response.setStatus(200);
- response.setContentType("application/json");
- response.setCharacterEncoding("utf-8");
- response.getWriter().print(string);
- }
- catch (IOException e)
- {
- e.printStackTrace();
- }
- return null;
- }
- }
Enfin, écrivez la classe d'entité utilisateur correspondante
- package org.example.domain;
-
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
- import java.io.Serializable;
- import java.util.Date;
- /**
- * 用户表(User)实体类
- */
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class User implements Serializable {
- private static final long serialVersionUID = -40356785423868312L;
- /**
- * 主键
- */
- private Long id;
- /**
- * 用户名
- */
- private String userName;
- /**
- * 昵称
- */
- private String nickName;
- /**
- * 密码
- */
- private String password;
- /**
- * 账号状态(0正常 1停用)
- */
- private String status;
- /**
- * 邮箱
- */
- private String email;
- /**
- * 手机号
- */
- private String phonenumber;
- /**
- * 用户性别(0男,1女,2未知)
- */
- private String sex;
- /**
- * 头像
- */
- private String avatar;
- /**
- * 用户类型(0管理员,1普通用户)
- */
- private String userType;
- /**
- * 创建人的用户id
- */
- private Long createBy;
- /**
- * 创建时间
- */
- private Date createTime;
- /**
- * 更新人
- */
- private Long updateBy;
- /**
- * 更新时间
- */
- private Date updateTime;
- /**
- * 删除标志(0代表未删除,1代表已删除)
- */
- private Integer delFlag;
- }
-
Selon notre analyse ci-dessus, nous devons personnaliser un UserDetailsService pour permettre à SpringSecuriry d'utiliser notre UserDetailsService. Notre propre UserDetailsService peut interroger le nom d'utilisateur et le mot de passe dans la base de données.
Nous créons d’abord une table de base de données sys_user.
- CREATE TABLE `sys_user` (
- `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
- `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
- `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '呢称',
- `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
- `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常1停用)',
- `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
- `phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
- `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
- `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
- `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(O管理员,1普通用户)',
- `create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
- `create_time` datetime DEFAULT NULL COMMENT '创建时间',
- `update_by` bigint DEFAULT NULL COMMENT '更新人',
- `update_time` datetime DEFAULT NULL COMMENT '更新时间',
- `del_flag` int DEFAULT '0' COMMENT '删除标志(O代表未删除,1代表已删除)',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
Présentez ensuite les pilotes myBatisPlus et mysql.
- <dependency>
- <groupId>com.baomidou</groupId>
- <artifactId>mybatis-plus-boot-starter</artifactId>
- <version>3.4.3</version>
- </dependency>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
Configurez ensuite les informations pertinentes de la base de données.
Définissez ensuite l'interface du mappeur UserMapper et utilisez mybatisplus pour ajouter les annotations correspondantes.
- package org.example.mapper;
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import org.example.domain.User;
-
- public interface UserMapper extends BaseMapper<User> {
- }
Configurez ensuite l'analyse des composants
Enfin, testez si le mp peut être utilisé normalement.
Présentation de Junit
De cette façon, il peut être utilisé normalement.
Ensuite, nous devons implémenter le code principal.
Tout d’abord, personnalisons UserDetailsService.
- package org.example.service.impl;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import org.example.domain.LoginUser;
- import org.example.domain.User;
- import org.example.mapper.UserMapper;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.core.userdetails.UserDetails;
- import org.springframework.security.core.userdetails.UserDetailsService;
- import org.springframework.security.core.userdetails.UsernameNotFoundException;
- import org.springframework.stereotype.Service;
- import java.util.Objects;
- @Service
- public class UserDetailsServiceImpl implements UserDetailsService {
- @Autowired
- private UserMapper userMapper;
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- //查询用户信息 [InMemoryUserDetailsManager是在内存中查找]
- LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(User::getUserName,username);
- User user = userMapper.selectOne(wrapper);
- //如果查询不到数据就抛出异常,给出提示
- if(Objects.isNull(user)){
- throw new RuntimeException("用户名或密码错误!");
- }
-
- //TODO 查询权限信息
-
- //封装为UserDetails对象返回
- return new LoginUser(user);
- }
- }
Ici, l'utilisateur est encapsulé sous UserDetails et renvoyé.
- package org.example.domain;
- import lombok.AllArgsConstructor;
- import lombok.Data;
- import lombok.NoArgsConstructor;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.userdetails.UserDetails;
- import java.util.Collection;
- @Data
- @AllArgsConstructor
- @NoArgsConstructor
- public class LoginUser implements UserDetails {
- private User user;
- @Override
- public Collection<? extends GrantedAuthority> getAuthorities() {
- return null;
- }
-
- @Override
- public String getPassword() {
- return user.getPassword();
- }
-
- @Override
- public String getUsername() {
- return user.getUserName();
- }
-
- @Override
- public boolean isAccountNonExpired() {
- return true;
- }
-
- @Override
- public boolean isAccountNonLocked() {
- return true;
- }
-
- @Override
- public boolean isCredentialsNonExpired() {
- return true;
- }
-
- @Override
- public boolean isEnabled() {
- return true;
- }
- }
Enfin, il y a un point ici, c'est-à-dire que nous devons effectuer un test de connexion pour obtenir les données de la base de données. Nous devons écrire les données utilisateur dans le tableau, et si vous souhaitez que le mot de passe de l'utilisateur soit transmis en texte clair. , vous devez ajouter {noop} avant le mot de passe.
Ici, vous pouvez saisir le nom d'utilisateur et le mot de passe dans la base de données pour vous connecter.
Parlons de la raison pour laquelle {noop} est ajouté devant le mot de passe, car le PasswordEncoder par défaut nécessite que le format du mot de passe dans la base de données soit {id}mot de passe. Il déterminera la méthode de cryptage du mot de passe en fonction de l'identifiant, mais nous en général. n'adoptez pas cette méthode, donc PasswordEncoder doit être remplacé.
Ensuite, nous allons le tester et voir.
Vous pouvez voir que les deux mots de passe d'origine que nous avons transmis ici sont les mêmes, mais nous avons obtenu des résultats différents. Cela est en fait lié à l'algorithme de salage. J'écrirai également un article sur le cryptage personnalisé plus tard.
Après avoir obtenu le mot de passe crypté, vous pouvez stocker le mot de passe crypté dans la base de données. Vous pouvez ensuite vous connecter en vérifiant le mot de passe en clair transmis depuis le front-end avec le mot de passe crypté dans la base de données.
À ce moment-là, nous avons démarré le projet pour nous connecter et avons constaté que nous ne pouvions plus nous connecter avec le mot de passe précédent, car la base de données devait stocker le mot de passe crypté stocké dans la base de données pendant la phase d'enregistrement, pas le mot de passe d'origine (car je Je ne me suis pas inscrit, je vais crypter le mot de passe) Le mot de passe est écrit tout seul dans la base de données).
Nous devons implémenter une interface de connexion, puis laisser SpringSecuruty l'autoriser. Si cela n'est pas autorisé, cela sera contradictoire. Dans l'interface, l'authentification de l'utilisateur est effectuée via la méthode d'authentification d'AuthentificationManager, nous devons donc configurer l'authentificationManager à injecter. dans le conteneur dans SecurityConfig.
Si l'authentification réussit, un jwt doit être généré et inséré dans la réponse. Pour que l'utilisateur puisse identifier l'utilisateur spécifique via le jwt lors de la prochaine requête, les informations utilisateur doivent être stockées dans Redis et l'utilisateur. L’ID peut être utilisé comme clé.
Écrivez d'abord LoginController
Écrivez ensuite le Service correspondant.
Injectez AuthenticationManager dans SecurityConfig et libérez l’interface de connexion.
Dans la logique métier du service, si l'authentification échoue, une exception personnalisée sera renvoyée, mais si l'authentification réussit, comment obtenons-nous les informations correspondantes.
Ici, nous pouvons déboguer et voir les objets obtenus.
On constate ici que les informations requises correspondantes peuvent être obtenues dans le Principal.
Complétez ensuite le code.
Enfin, testez-le.
Je vais d'abord coller le code.
- @Component
- public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
-
- @Autowired
- private RedisCache redisCache;
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- //获取token
- String token = request.getHeader("token");
- if (!StringUtils.hasText(token)) {
- //放行
- filterChain.doFilter(request, response); //这里放行是因为还有后续的过滤器会给出对应的异常
- return; //token为空 不执行后续流程
- }
- //解析token
- String userid;
- try {
- Claims claims = JwtUtil.parseJWT(token);
- userid = claims.getSubject();
- } catch (Exception e) {
- e.printStackTrace();
- throw new RuntimeException("token非法!");
- }
- //从redis中获取用户信息
- String redisKey = "login:" + userid;
- LoginUser loginUser = redisCache.getCacheObject(redisKey);
- if (Objects.isNull(loginUser)){
- throw new RuntimeException("用户未登录!");
- }
- //将信息存入SecurityContextHolder(因为过滤器链后面的filter都是从中获取认证信息进行对应放行)
- //TODO 获取权限信息封装到Authentication中
- UsernamePasswordAuthenticationToken authenticationToken =
- new UsernamePasswordAuthenticationToken(loginUser,null,null);
- SecurityContextHolder.getContext().setAuthentication(authenticationToken);
-
- //放行
- filterChain.doFilter(request,response); //此时的放行是携带认证的,不同于上方token为空的放行
- }
- }
Tout d'abord, pour obtenir le jeton ici, nous récupérons le jeton correspondant de l'en-tête de la requête, puis vérifions qu'il est vide, nous le libérerons directement sans passer par le processus de suivi. analysera le jeton, obtiendra l'ID utilisateur à l'intérieur, puis utilisera l'ID utilisateur en fonction de l'obtention des informations utilisateur correspondantes à partir de Redis, et enfin les stockera dans SecurityContextHolder, car les filtres suivants doivent en obtenir les informations d'authentification quotidiennes et enfin effectuer opérations d’analyse.
Un autre point qui mérite attention est que SecurityContextHolder.getContext().setAuthentication() doit transmettre l'objet d'authentification. Lorsque nous construisons l'objet, nous utilisons trois paramètres, car le troisième paramètre est la clé pour déterminer s'il faut s'authentifier ou non.
Ensuite, nous devons configurer ce filtre.
Ensuite, lorsque nous accéderons à l'interface utilisateur/connexion, un corps de réponse avec un jeton nous sera renvoyé Lorsque nous accéderons à nouveau à l'interface hello, ce sera 403. Comme il ne contient pas de jeton, il correspond au code ci-dessus. Sans jeton, le corps de la réponse sera libéré et le retour n'exécutera pas le processus suivant (la version ici est due au fait qu'il existe d'autres filtres qui lèvent spécifiquement des exceptions pour un traitement ultérieur, et le retour vise à l'empêcher de passer par la réponse. processus)
À ce stade, si nous mettons le jeton généré par l'utilisateur/connexion dans l'en-tête de requête de l'interface hello, nous pouvons y accéder normalement.
Ensuite, l'objectif de notre ensemble de filtres a été atteint (obtention de jetons, analyse des jetons et stockage dans SecurityContextHolder).
À ce stade, il nous est plus facile de nous déconnecter. Il nous suffit de supprimer les données correspondantes dans redis. Lorsque nous apporterons le jeton pour un accès ultérieur, les informations utilisateur correspondantes dans redis seront obtenues dans notre filtre personnalisé à ce moment-là. vous ne pouvez pas l'obtenir, cela signifie que vous n'êtes pas connecté.
Nous transportons ce jeton pour accéder à l'interface /user/logout.
Ensuite, la fonction de déconnexion est implémentée.
Cet article a été tiré de la troisième mise à jour de la station b ! ! !