Mi informacion de contacto
Correo[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Tabla de contenido
2.1 Proceso de verificación de inicio de sesión
2.2 Exploración preliminar del principio
2.3.3.1 Usuario de verificación de base de datos
2.3.3.2 Almacenamiento cifrado con contraseña
2.3.3.3 Interfaz de inicio de sesión
2.3.3.4 Filtro de autenticación
Spring Security es un marco de gestión de seguridad de la familia Spring. En comparación con Shiro, otro marco de seguridad, proporciona funciones y recursos comunitarios más ricos que Shiro.
En términos generales, Spring Security se usa más comúnmente en proyectos a gran escala, y Shiro se usa más comúnmente en proyectos pequeños, porque en comparación con Spring Security, es más fácil comenzar con Shiro.
Las aplicaciones web generales debenCertificaciónyAutorizar。
¡La autenticación y la autorización son las funciones principales de Spring Security como marco de seguridad!
Primero, simplemente construyamos un proyecto SpringBoot.
En este momento, accedemos a una interfaz de saludo simple que escribimos para verificar si la construcción fue exitosa.
Luego presente SpringSecurity.
En este momento, echemos un vistazo al efecto de la interfaz de acceso.
Después de introducir SpringSecurity, la interfaz de acceso saltará automáticamente a una página de inicio de sesión. El nombre de usuario predeterminado es usuario y la contraseña se enviará a la consola. Debe iniciar sesión para acceder a la interfaz.
En primer lugar, debemos comprender el proceso de verificación de inicio de sesión. Primero, la interfaz lleva el nombre de usuario y la contraseña para acceder a la interfaz de inicio de sesión. Después de que el servidor obtiene el nombre de usuario y la contraseña, los compara con los de la base de datos. Si el nombre de usuario / ID de usuario se usa correctamente, se genera un jwt y luego Responde jwt al front-end y luego accede a otras solicitudes después de iniciar sesión y llevará el token en el encabezado de la solicitud Cada vez que el servidor obtenga el token. el encabezado de la solicitud para el análisis, obtiene la ID de usuario y obtiene información relacionada con el usuario y permisos de visualización en función de la ID del nombre de usuario. Si tiene permiso, responda al front-end.
El principio de SpringSecurity es en realidad una cadena de filtros, que proporciona filtros con varias funciones. Aquí primero veremos los filtros involucrados en el inicio rápido anterior.
También podemos usar Debug para ver qué filtros están en la cadena de filtros SpringSecurity en el sistema actual y su orden.
A continuación, echemos un vistazo al análisis del diagrama de flujo de autenticación.
Aquí solo necesitamos poder entender el proceso. En pocas palabras:
El usuario envió el nombre de usuario y la contraseña, UsernamePasswordAuthenticationFilter lo encapsula como un objeto de autenticación y llama al método de autenticación para la autenticación. Luego llama al método de autenticación de DaoAuthenticationProvider para la autenticación y luego llama al método loadUserByUserName para consultar al usuario. es buscar en la memoria y luego encapsular la información del usuario correspondiente en un objeto UserDetails, usar PasswordEncoder para comparar la contraseña en UserDetails y la contraseña de autenticación para ver si es correcta. Si es correcta, establezca la información de permiso en UserDetails en el objeto de autenticación. , luego devuelve el objeto de autenticación y finalmente usa el método SecurityContextHolder.getContext( ).setAuthentication almacena este objeto y otros filtros obtendrán la información del usuario actual a través de SecurityContextHoder. (No es necesario memorizar este párrafo para entenderlo)
Luego conocemos el proceso antes de poder modificarlo. En primer lugar, cuando buscamos desde la memoria, debemos buscar desde la base de datos (aquí necesitamos personalizar una clase de implementación UserDetailsService) y no usaremos el nombre de usuario y la contraseña predeterminados. Y la interfaz de inicio de sesión debe ser escrita por usted mismo, y no es necesario utilizar la página de inicio de sesión predeterminada proporcionada por él.
Según la situación que analizamos, podemos obtener esa imagen.
En este momento, se devuelve un jwt al front-end y otras solicitudes realizadas por el front-end transportarán el token. Entonces, nuestro primer paso es verificar si el token se transporta, analizarlo, obtener el ID de usuario correspondiente y encapsularlo. it as El objeto Anthentication se almacena en SecurityContextHolder (para que otros filtros puedan obtenerlo).
Entonces hay otra pregunta aquí. ¿Cómo obtener información completa del usuario después de obtener el ID de usuario del filtro de autenticación jwt?
Aquí usamos redis. Cuando el servidor se autentica usando la identificación de usuario para generar jwt en el front-end, la identificación de usuario se usa como clave y la información del usuario se almacena en redis como valor. Luego se puede obtener la información completa del usuario. desde redis a través del ID de usuario.
A partir de la exploración preliminar de los principios anteriores, también hemos analizado aproximadamente lo que debemos hacer si implementamos el proceso de autenticación de separación de front-end y back-end por nuestra cuenta.
Acceso:
a. interfaz de inicio de sesión personalizada
Llame al método ProviderManager para la autenticación. Si la autenticación pasa, se genera un jwt.
Almacenar información del usuario en redis
b. Personalizar el servicio de detalles del usuario
Consultar la base de datos en esta clase de implementación.
controlar:
a. Personalizar el filtro de autenticación jwt.
Obtener token
Analizar el token para obtener su ID de usuario.
Obtenga información completa del usuario de redis
Almacenar en SecurityContextHolder
Primero necesitas agregar las dependencias correspondientes.
- <!-- 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>
Luego necesitamos usar Redis y agregar configuraciones relacionadas con Redis.
El primero es el serializador 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);
- }
-
- }
Cree RedisConfig y cree un serializador en él para resolver problemas como caracteres confusos.
- 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;
- }
- }
También es necesario unificar la clase de respuesta.
- 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;
- }
- }
Necesita clases de herramientas jwt para generar jwt y analizar 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();
- }
- }
Defina otra clase de herramienta Redis, RedisCache, que nos facilite llamar a 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);
- }
- }
-
También podemos escribir datos en la respuesta, por lo que también necesitamos una clase de herramienta 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;
- }
- }
Finalmente escriba la clase de entidad de usuario correspondiente.
- 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;
- }
-
Según nuestro análisis anterior, necesitamos personalizar un UserDetailsService para permitir que SpringSecuriry use nuestro UserDetailsService. Nuestro propio UserDetailsService puede consultar el nombre de usuario y la contraseña de la base de datos.
Primero creamos una tabla de base de datos 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='用户表';
Luego introduzca los controladores myBatisPlus y 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>
Luego configure la información relevante de la base de datos.
Luego defina la interfaz del mapeador UserMapper y use mybatisplus para agregar las anotaciones correspondientes.
- package org.example.mapper;
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import org.example.domain.User;
-
- public interface UserMapper extends BaseMapper<User> {
- }
Luego configure el escaneo de componentes
Finalmente, pruebe si el mp se puede utilizar normalmente.
Introducir junit
De esta forma se podrá utilizar con normalidad.
A continuación necesitamos implementar el código central.
Primero, personalicemos 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);
- }
- }
Aquí el usuario se encapsula como UserDetails y se devuelve.
- 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;
- }
- }
Finalmente, hay un punto aquí, es decir, debemos realizar una prueba de inicio de sesión para obtener datos de la base de datos. Necesitamos escribir los datos del usuario en la tabla y, si desea, la contraseña del usuario se transmitirá en texto sin cifrar. , debes agregar {noop} antes de la contraseña.
Aquí puede ingresar el nombre de usuario y contraseña en la base de datos para iniciar sesión.
Hablemos de por qué se agrega {noop} delante de la contraseña, porque el PasswordEncoder predeterminado requiere que el formato de contraseña en la base de datos sea {id}contraseña. Determinará el método de cifrado de la contraseña según la identificación, pero generalmente lo hacemos. no adopte este método, por lo que es necesario reemplazar PasswordEncoder.
A continuación lo probaremos y veremos.
Puede ver que las dos contraseñas originales que ingresamos aquí son las mismas, pero obtuvimos resultados diferentes. En realidad, esto está relacionado con el algoritmo de sal. También escribiré un artículo sobre cifrado personalizado más adelante.
Después de obtener la contraseña cifrada, puede almacenarla en la base de datos. Luego puede iniciar sesión verificando la contraseña en texto plano pasada desde el front-end con la contraseña cifrada en la base de datos.
En este momento, iniciamos el proyecto para iniciar sesión y descubrimos que ya no podíamos iniciar sesión con la contraseña anterior, porque la base de datos debería almacenar la contraseña cifrada almacenada en la base de datos durante la fase de registro, no la contraseña original (porque yo no se registró, cifraré la contraseña) La contraseña se escribe sola en la base de datos).
Necesitamos implementar una interfaz de inicio de sesión y luego dejar que SpringSecuruty lo permita. Si no está permitido, será contradictorio que la autenticación del usuario se realice a través del método de autenticación de AuthenticationManager en la interfaz, por lo que debemos configurar el AuthenticationManager para que se inyecte. en el contenedor en SecurityConfig.
Si la autenticación es exitosa, se debe generar un jwt y colocarlo en la respuesta. Para que el usuario identifique al usuario específico a través del jwt al realizar la siguiente solicitud, la información del usuario debe almacenarse en redis y el usuario. La identificación se puede utilizar como clave.
Escribe LoginController primero
Luego escribe el Servicio correspondiente.
Inyecte AuthenticationManager en SecurityConfig y libere la interfaz de inicio de sesión.
En la lógica empresarial del servicio, si la autenticación falla, se devolverá una excepción personalizada, pero si la autenticación es exitosa, ¿cómo obtenemos la información correspondiente?
Aquí podemos depurar y ver los objetos obtenidos.
Se encuentra aquí que la información requerida correspondiente se puede obtener en el Principal.
Luego completa el código.
Finalmente, pruébalo.
Primero pegaré el código.
- @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为空的放行
- }
- }
En primer lugar, para obtener el token aquí, obtenemos el token correspondiente del encabezado de la solicitud y luego verificamos que esté vacío. Si está vacío, lo liberaremos directamente sin pasar por el proceso de seguimiento. analizará el token, obtendrá el ID de usuario dentro y luego usará el ID de usuario según Obtenga la información de usuario correspondiente de redis y finalmente la almacenará en SecurityContextHolder, porque los filtros posteriores deben obtener la información de autenticación diaria de él y finalmente realizar operaciones de análisis.
Otro punto que necesita atención es que SecurityContextHolder.getContext().setAuthentication() necesita pasar el objeto de autenticación. Cuando construimos el objeto, usamos tres parámetros, porque el tercer parámetro es la clave para determinar si autenticar o no.
A continuación necesitamos configurar este filtro.
Luego, cuando accedamos a la interfaz de usuario/inicio de sesión, se nos devolverá un cuerpo de respuesta con un token. Cuando accedamos a la interfaz de saludo nuevamente, será 403. Debido a que no lleva un token, corresponde al código anterior. Sin un token, el cuerpo de la respuesta se liberará y la devolución no ejecutará el proceso posterior (la liberación aquí se debe a que hay otros filtros que arrojan excepciones específicamente para su procesamiento posterior, y la devolución es para evitar que pase por la respuesta. proceso)
En este momento, si colocamos el token generado por el usuario/inicio de sesión en el encabezado de solicitud de la interfaz de saludo, podemos acceder a él normalmente.
Luego se logró el propósito de nuestro conjunto de filtros (obtener tokens, analizar tokens y almacenarlos en SecurityContextHolder)
En este punto, es más fácil para nosotros cerrar sesión. Solo necesitamos eliminar los datos correspondientes en redis. Cuando traigamos el token para acceder más tarde, la información del usuario correspondiente en redis se obtendrá en nuestro filtro personalizado. No puedes obtenerlo, significa que no has iniciado sesión.
Llevamos este token para acceder a la interfaz /user/logout.
Luego se implementa la función de cierre de sesión.
¡Este artículo se aprendió de la tercera actualización de la estación b! ! !