minhas informações de contato
Correspondência[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Índice
2.1 Processo de verificação de login
2.2 Exploração preliminar do princípio
2.3.3.1 Usuário de verificação de banco de dados
2.3.3.2 Armazenamento criptografado por senha
2.3.3.4 Filtro de autenticação
Spring Security é uma estrutura de gerenciamento de segurança da família Spring. Comparada com Shiro, outra estrutura de segurança, ela fornece funções e recursos de comunidade mais ricos do que Shiro.
De modo geral, Spring Security é mais comumente usado em projetos de grande escala, e Shiro é mais comumente usado em pequenos projetos, porque comparado ao Spring Security, Shiro é mais fácil de começar.
Aplicativos web gerais precisamCertificaçãoeAutorizar。
Autenticação e autorização são as principais funções do Spring Security como estrutura de segurança!
Vamos primeiro simplesmente construir um projeto SpringBoot.
Neste momento, acessamos uma interface simples de hello que escrevemos para verificar se a construção foi bem-sucedida.
Em seguida, apresente o SpringSecurity.
Neste momento, vamos dar uma olhada no efeito da interface de acesso.
Após a introdução do SpringSecurity, a interface de acesso irá automaticamente para uma página de login. O nome de usuário padrão é user e a senha será enviada para o console.
Primeiro de tudo, precisamos entender o processo de verificação de login. Primeiro, o front end carrega o nome de usuário e a senha para acessar a interface de login. Depois que o servidor obtém o nome de usuário e a senha, ele os compara com os do banco de dados. Se o nome de usuário/ID do usuário for usado corretamente, um jwt será gerado e, em seguida, responder jwt ao front-end e, em seguida, acessar outras solicitações após o login carregará o token no cabeçalho da solicitação. o cabeçalho da solicitação para análise, obtém o UserID e obtém informações relacionadas ao usuário e permissões do visualizador com base no ID do nome de usuário. Se você tiver permissão, responda ao front-end.
O princípio do SpringSecurity é na verdade uma cadeia de filtros, que fornece filtros com várias funções. Aqui veremos primeiro os filtros envolvidos no início rápido acima.
Também podemos usar Debug para ver quais filtros estão na cadeia de filtros SpringSecurity no sistema atual e sua ordem.
A seguir, vamos dar uma olhada na análise do fluxograma de autenticação.
Aqui só precisamos ser capazes de entender o processo. Para simplificar:
O usuário enviou o nome de usuário e a senha, UsernamePasswordAuthenticationFilter encapsula-o como um objeto Authentication e chama o método authenticate para autenticação. Em seguida, ele chama o método authenticate de DaoAuthenticationProvider para autenticação e, em seguida, chama o método loadUserByUserName para consultar o usuário. é pesquisar na memória e, em seguida, encapsular as informações do usuário correspondentes em um objeto UserDetails, use PasswordEncoder para comparar a senha em UserDetails e a senha de autenticação para ver se está correta, defina as informações de permissão em UserDetails para o objeto de autenticação. , retorne o objeto Authentication e, finalmente, use o método SecurityContextHolder.getContext( ).setAuthentication armazena esse objeto e outros filtros obterão as informações do usuário atual por meio de SecurityContextHoder. (Você não precisa memorizar este parágrafo para entendê-lo)
Então conhecemos o processo antes de podermos modificá-lo. Em primeiro lugar, ao pesquisar na memória, devemos pesquisar no banco de dados (aqui precisamos personalizar uma classe de implementação UserDetailsService) e não usaremos o nome de usuário e a senha padrão. , e a interface de login deve ser escrita por você mesmo, e não há necessidade de usar a página de login padrão fornecida por ele.
Com base na situação que analisamos, podemos obter esse quadro.
Neste momento, um jwt é retornado ao front-end. Neste momento, outras solicitações feitas pelo front-end carregarão o token. Portanto, nosso primeiro passo é verificar se o token é transportado, analisar o token e obter o ID do usuário correspondente. e encapsule-o como O objeto Anthentication é armazenado no SecurityContextHolder (para que outros filtros possam obtê-lo).
Portanto, há outra questão aqui. Como obter informações completas do usuário após obter o ID do usuário do filtro de autenticação jwt?
Aqui usamos redis. Quando o servidor autentica usando o ID do usuário para gerar jwt para o front-end, o ID do usuário é usado como chave e as informações do usuário são armazenadas no redis como o valor. do redis por meio do ID do usuário.
A partir da exploração preliminar dos princípios acima, também analisamos aproximadamente o que precisamos fazer se implementarmos nós mesmos o processo de autenticação de separação de front-end e back-end.
Conecte-se:
a. Interface de login personalizada
Chame o método ProviderManager para autenticação. Se a autenticação for aprovada, um jwt será gerado.
Armazene informações do usuário no redis
b. Personalizar UserDetailsService
Consulte o banco de dados nesta classe de implementação
verificar:
a. Personalize o filtro de autenticação jwt
Obter token
Analise o token para obter seu ID de usuário
Obtenha informações completas do usuário do redis
Armazenar em SecurityContextHolder
Primeiro você precisa adicionar as dependências correspondentes
- <!-- 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>
Então precisamos usar o Redis e adicionar configurações relacionadas ao Redis.
O primeiro é o serializador do 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);
- }
-
- }
Crie RedisConfig e crie um serializador nele para resolver problemas como caracteres ilegíveis.
- 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;
- }
- }
Também é necessário unificar a classe de resposta
- 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;
- }
- }
Você precisa de classes de ferramentas jwt para gerar jwt e analisar 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 outra classe de ferramenta Redis RedisCache, o que torna mais fácil para nós chamarmos 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);
- }
- }
-
Também podemos escrever dados na resposta, então também precisamos de uma classe de ferramenta 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 escreva a classe de entidade do usuário correspondente
- 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;
- }
-
De acordo com nossa análise acima, precisamos customizar um UserDetailsService para permitir que SpringSecuriry use nosso UserDetailsService. Nosso próprio UserDetailsService pode consultar o nome de usuário e a senha do banco de dados.
Primeiro criamos uma tabela de banco de dados 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='用户表';
Em seguida, apresente os drivers myBatisPlus e 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>
Em seguida, configure as informações relevantes do banco de dados.
Em seguida, defina a interface do mapeador UserMapper e use mybatisplus para adicionar as anotações correspondentes.
- package org.example.mapper;
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import org.example.domain.User;
-
- public interface UserMapper extends BaseMapper<User> {
- }
Em seguida, configure a verificação de componentes
Finalmente, teste se o mp pode ser usado normalmente.
Introduzir junit
Desta forma pode ser usado normalmente.
Em seguida, precisamos implementar o código principal.
Primeiro, vamos personalizar 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);
- }
- }
Aqui o usuário é encapsulado como UserDetails e retornado.
- 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;
- }
- }
Por fim, há um ponto aqui, ou seja, precisamos realizar um teste de login para obter os dados do banco de dados. Precisamos escrever os dados do usuário na tabela, e se quisermos que a senha do usuário seja transmitida em texto não criptografado. , você precisa adicionar {noop} antes da senha.
Aqui você pode inserir o nome de usuário e a senha do banco de dados para fazer login.
Vamos falar sobre por que {noop} deve ser adicionado na frente da senha, porque o PasswordEncoder padrão exige que o formato da senha no banco de dados seja {id}password. Ele determinará o método de criptografia da senha com base no id, mas nós determinaremos o método de criptografia da senha com base no id. geralmente não usa esse método, então o PasswordEncoder precisa ser substituído.
A seguir vamos testá-lo e ver.
Você pode ver que as duas senhas originais que passamos aqui são iguais, mas obtivemos resultados diferentes. Na verdade, isso está relacionado ao algoritmo de salting. Escreverei um artigo sobre criptografia personalizada mais tarde.
Depois de obter a senha criptografada, você pode armazená-la no banco de dados. Em seguida, você pode fazer login verificando a senha em texto simples passada do front end com a senha criptografada no banco de dados.
Neste momento, iniciamos o projeto de login e descobrimos que não poderíamos mais fazer login com a senha anterior, pois o banco de dados deveria armazenar a senha criptografada armazenada no banco de dados durante a fase de registro, e não a senha original (porque eu não registrei, vou criptografar a senha) A senha é gravada sozinha no banco de dados).
Precisamos implementar uma interface de login e então deixar o SpringSecuruty permitir. Se não for permitido, será contraditório. Na interface, a autenticação do usuário é realizada através do método authenticate do AuthenticationManager, então precisamos configurar o AuthenticationManager para ser injetado. no contêiner em SecurityConfig.
Se a autenticação for bem-sucedida, um jwt precisa ser gerado e colocado na resposta. Para que o usuário identifique o usuário específico por meio do jwt ao fazer a próxima solicitação, as informações do usuário precisam ser armazenadas no redis e o usuário. O ID pode ser usado como chave.
Escreva LoginController primeiro
Em seguida, escreva o Serviço correspondente.
Injete AuthenticationManager em SecurityConfig e libere a interface de login.
Na lógica de negócio do serviço, se a autenticação falhar, uma exceção customizada será retornada, mas se a autenticação for bem-sucedida, como obtemos as informações correspondentes.
Aqui podemos depurar e ver os objetos obtidos.
Verifica-se aqui que as informações necessárias correspondentes podem ser obtidas no Diretor.
Em seguida, complete o código.
Finalmente, teste-o.
Vou colar o código primeiro.
- @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为空的放行
- }
- }
Em primeiro lugar, para obter o token aqui, obtemos o token correspondente do cabeçalho da solicitação e, em seguida, verificamos se está vazio, iremos liberá-lo diretamente, sem passar pelo processo de acompanhamento. irá analisar o token, obter o ID do usuário dentro e, em seguida, com base no ID do usuário, obter as informações do usuário correspondentes do redis e, finalmente, armazená-las no SecurityContextHolder, porque os filtros subsequentes precisam obter as informações de autenticação diárias dele e, finalmente, realizar a análise. operações.
Outro ponto a ser observado é que SecurityContextHolder.getContext().setAuthentication() precisa passar o objeto de autenticação. Quando construímos o objeto, usamos três parâmetros, pois o terceiro parâmetro é a chave para determinar se devemos autenticar ou não.
Em seguida, precisamos configurar este filtro.
Então, quando acessarmos a interface usuário/login, um corpo de resposta com um token será retornado para nós. Quando acessarmos a interface hello novamente, será 403. Por não conter token, corresponde ao código acima. Se não houver token, o corpo da resposta será liberado e o retorno não executará o processo subsequente (A liberação aqui é porque existem outros filtros que lançam especificamente exceções para processamento posterior, e o retorno é para evitar que ele passe. o processo de resposta)
Neste momento, se colocarmos o token gerado pelo usuário/login no cabeçalho da solicitação da interface hello, poderemos acessá-lo normalmente.
Então o objetivo do nosso conjunto de filtros foi alcançado (obter tokens, analisar tokens e armazená-los em SecurityContextHolder)
Neste ponto, é mais fácil sair. Só precisamos excluir os dados correspondentes no redis. Quando trouxermos o token para acesso posterior, as informações do usuário correspondentes no redis serão obtidas em nosso filtro personalizado. Se você não conseguir, significa que você não está logado.
Carregamos esse token para acessar a interface /user/logout.
Em seguida, a função de logout é implementada.
Este artigo foi aprendido na terceira atualização da estação b! ! !