моя контактная информация
Почтамезофия@protonmail.com
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Оглавление
2.2 Предварительное исследование принципа
2.3.3.1 Пользователь проверки базы данных
2.3.3.2 Хранилище, зашифрованное паролем
2.3.3.3 Интерфейс входа в систему
Spring Security — это платформа управления безопасностью в семействе Spring. По сравнению с Shiro, другой структурой безопасности, она предоставляет более широкие функции и более богатые ресурсы сообщества, чем Shiro.
Вообще говоря, Spring Security чаще используется в крупномасштабных проектах, а Shiro чаще используется в небольших проектах, поскольку по сравнению со Spring Security, с Shiro легче начать работу.
Общие веб-приложения должныСертификацияиАвторизовать。
Аутентификация и авторизация — основные функции Spring Security как структуры безопасности!
Давайте сначала просто создадим проект SpringBoot.
На данный момент мы получаем доступ к простому интерфейсу приветствия, который мы написали, чтобы проверить, успешно ли построено.
Затем представьте SpringSecurity.
А теперь давайте посмотрим на эффект интерфейса доступа.
После внедрения SpringSecurity интерфейс доступа автоматически перейдет на страницу входа. Имя пользователя по умолчанию — user, а пароль будет выведен на консоль. Для доступа к интерфейсу необходимо войти в систему.
Прежде всего, нам нужно понять процесс проверки входа. Во-первых, внешний интерфейс содержит имя пользователя и пароль для доступа к интерфейсу входа. После того, как сервер получает имя пользователя и пароль, он сравнивает их с теми, что находятся в базе данных. Если имя пользователя/идентификатор пользователя используются правильно, генерируется jwt, а затем ответ jwt во внешний интерфейс, а затем доступ к другим запросам после входа в систему, будет содержать токен в заголовке запроса каждый раз, когда сервер получает токен. заголовок запроса для анализа, получает UserID и получает информацию, связанную с пользователем, и разрешения просмотра на основе идентификатора имени пользователя. Если у вас есть разрешение, ответьте на внешний интерфейс.
Принцип SpringSecurity на самом деле представляет собой цепочку фильтров, которая предоставляет фильтрам различные функции. Здесь мы сначала рассмотрим фильтры, задействованные в приведенном выше быстром запуске.
Мы также можем использовать Debug, чтобы увидеть, какие фильтры находятся в цепочке фильтров SpringSecurity в текущей системе и их порядок.
Далее давайте посмотрим на анализ блок-схемы аутентификации.
Здесь нам нужно только уметь понимать процесс. Если говорить проще:
Пользователь предоставил имя пользователя и пароль, UsernamePasswordAuthenticationFilter инкапсулирует его как объект Authentication и вызывает метод аутентификации для аутентификации. Затем он вызывает метод аутентификации DaoAuthenticationProvider для аутентификации, а затем вызывает метод loadUserByUserName для запроса пользователя. заключается в поиске в памяти, а затем инкапсуляции соответствующей информации о пользователе в объект UserDetails, использовании PasswordEncoder для сравнения пароля в UserDetails и пароля аутентификации, чтобы убедиться, что он верен, установите информацию о разрешениях в UserDetails для объекта Authentication. , затем верните объект аутентификации и, наконец, используйте метод SecurityContextHolder.getContext().setAuthentication, сохраняющий этот объект, а другие фильтры получат информацию о текущем пользователе через SecurityContextHoder. (Вам не нужно запоминать этот абзац, чтобы понять его)
Тогда мы узнаем процесс, прежде чем сможем его изменить. Прежде всего, при поиске в памяти мы должны искать в базе данных (здесь нам нужно настроить класс реализации UserDetailsService), и мы не будем использовать имя пользователя и пароль по умолчанию. , а интерфейс входа должен быть написан вами самостоятельно, и нет необходимости использовать предоставленную им страницу входа по умолчанию.
Исходя из ситуации, которую мы проанализировали, мы можем получить такую картину.
В это время во внешний интерфейс возвращается jwt, и другие запросы, сделанные внешним интерфейсом, будут нести токен. Поэтому наш первый шаг — проверить, переносится ли токен, проанализировать токен, получить соответствующий идентификатор пользователя и инкапсулировать. он как объект Anthentication хранится в SecurityContextHolder (чтобы его могли получить другие фильтры).
Итак, здесь возникает еще один вопрос. Как получить полную информацию о пользователе после получения идентификатора пользователя из фильтра аутентификации jwt?
Здесь мы используем redis. Когда сервер проверяет подлинность, используя идентификатор пользователя для генерации jwt во внешнем интерфейсе, идентификатор пользователя используется в качестве ключа, а информация о пользователе сохраняется в redis в качестве значения. Тогда можно получить полную информацию о пользователе. из Redis через идентификатор пользователя.
Из предварительного изучения вышеизложенных принципов мы также грубо проанализировали, что нам нужно делать, если мы реализуем процесс аутентификации для разделения внешнего и внутреннего интерфейса самостоятельно.
Авторизоваться:
а. Пользовательский интерфейс входа.
Вызовите метод ProviderManager для аутентификации. Если аутентификация пройдена, создается jwt.
Хранить информацию о пользователе в Redis
б. Настройте службу UserDetailsService.
Запросить базу данных в этом классе реализации
проверять:
а. Настройте фильтр аутентификации jwt.
Получить токен
Разберите токен, чтобы получить его идентификатор пользователя.
Получить полную информацию о пользователе из Redis
Хранить в SecurityContextHolder
Сначала вам нужно добавить соответствующие зависимости
- <!-- 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>
Затем нам нужно использовать Redis и добавить конфигурации, связанные с Redis.
Во-первых, это сериализатор 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);
- }
-
- }
Создайте RedisConfig и создайте в нем сериализатор для решения таких проблем, как искажение символов.
- 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;
- }
- }
Также необходимо унифицировать класс ответа
- 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;
- }
- }
Вам нужны классы инструментов jwt для генерации jwt и анализа 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();
- }
- }
Определите еще один класс инструмента Redis RedisCache, который облегчит нам вызов 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);
- }
- }
-
Мы также можем записывать данные в ответ, поэтому нам также понадобится класс инструмента 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;
- }
- }
Наконец, напишите соответствующий класс пользовательской сущности.
- 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;
- }
-
Согласно нашему анализу, приведенному выше, нам необходимо настроить UserDetailsService, чтобы SpringSecuriry мог использовать наш UserDetailsService. Наша собственная служба UserDetailsService может запрашивать имя пользователя и пароль из базы данных.
Сначала мы создаем таблицу базы данных 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='用户表';
Затем представьте драйверы myBatisPlus и 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>
Затем настройте соответствующую информацию базы данных.
Затем определите интерфейс картографа UserMapper и используйте mybatisplus, чтобы добавить соответствующие аннотации.
- package org.example.mapper;
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import org.example.domain.User;
-
- public interface UserMapper extends BaseMapper<User> {
- }
Затем настройте сканирование компонентов
Наконец, проверьте, можно ли нормально использовать MP.
Представляем Джунит
Таким образом, его можно использовать в обычном режиме.
Далее нам нужно реализовать основной код.
Сначала давайте настроим 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);
- }
- }
Здесь пользователь инкапсулируется как UserDetails и возвращается.
- 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;
- }
- }
Наконец, здесь есть момент, то есть нам нужно выполнить тест входа в систему, чтобы получить данные из базы данных. Нам нужно записать данные пользователя в таблицу, и если вы хотите, чтобы пароль пользователя передавался в открытом виде. , вам нужно добавить {noop} перед паролем.
Здесь вы можете ввести логин и пароль в базе данных для входа.
Давайте поговорим о том, почему перед паролем добавляется {noop}, поскольку для пароля по умолчанию требуется, чтобы формат пароля в базе данных был {id}password. Он определяет метод шифрования пароля на основе идентификатора, но обычно мы говорим о том, почему {noop} добавляется перед паролем. не используйте этот метод, поэтому необходимо заменить PasswordEncoder.
Дальше проверим и посмотрим.
Вы можете видеть, что два исходных пароля, которые мы здесь передали, одинаковы, но мы получили разные результаты. На самом деле это связано с алгоритмом добавления. Позже я также напишу статью о специальном шифровании.
После получения зашифрованного пароля вы можете сохранить зашифрованный пароль в базе данных. Затем вы можете войти в систему, проверив пароль в виде открытого текста, переданный из внешнего интерфейса, с зашифрованным паролем в базе данных.
В это время мы запустили проект по входу в систему и обнаружили, что больше не можем войти в систему с предыдущим паролем, поскольку база данных должна хранить зашифрованный пароль, хранящийся в базе данных на этапе регистрации, а не исходный пароль (потому что я не регистрировался, пароль зашифрую) Пароль записывается в базу сам).
Нам нужно реализовать интерфейс входа в систему, а затем позволить SpringSecuruty разрешить это. Если это не разрешено, это будет противоречиво. В интерфейсе аутентификация пользователя выполняется с помощью метода аутентификации AuthenticationManager, поэтому нам нужно настроить внедрение AuthenticationManager. в контейнер в SecurityConfig.
Если аутентификация прошла успешно, необходимо сгенерировать jwt и поместить его в ответ. Чтобы пользователь мог идентифицировать конкретного пользователя через jwt при выполнении следующего запроса, информация о пользователе должна быть сохранена в Redis, а сам пользователь. В качестве ключа можно использовать идентификатор.
Сначала напишите LoginController
Затем напишите соответствующий Сервис.
Вставьте AuthenticationManager в SecurityConfig и освободите интерфейс входа.
В бизнес-логике сервиса, если аутентификация не удалась, будет возвращено пользовательское исключение, но если аутентификация прошла успешно, как мы получим соответствующую информацию.
Здесь мы можем отладить и посмотреть полученные объекты.
Здесь установлено, что соответствующую необходимую информацию можно получить у Принципала.
Затем заполните код.
Наконец, протестируйте это.
Сначала я вставлю код.
- @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为空的放行
- }
- }
Прежде всего, чтобы получить токен здесь, мы получаем соответствующий токен из заголовка запроса, а затем проверяем его на пустое состояние, мы сразу освобождаем его, не проходя последующий процесс. будет анализировать токен, получать идентификатор пользователя внутри, а затем использовать идентификатор пользователя на основе Получение соответствующей информации о пользователе из Redis и, наконец, сохранит его в SecurityContextHolder, поскольку последующие фильтры должны получать от него ежедневную информацию аутентификации и, наконец, выполнять аналитические операции.
Еще один момент, требующий внимания, заключается в том, что SecurityContextHolder.getContext().setAuthentication() необходимо передать объект аутентификации. Когда мы создаем объект, мы используем три параметра, поскольку третий параметр является ключом для определения необходимости аутентификации.
Далее нам нужно настроить этот фильтр.
Затем, когда мы получим доступ к интерфейсу пользователя/входа, нам будет возвращено тело ответа с токеном. Когда мы снова обратимся к интерфейсу hello, он будет 403. Поскольку он не несет токена, он соответствует приведенному выше коду. Без токена тело ответа будет освобождено, и возврат не выполнит последующий процесс (освобождение здесь связано с тем, что существуют другие фильтры, которые специально создают исключения для последующей обработки, а возврат предназначен для предотвращения прохождения ответа. процесс)
В настоящее время, если мы поместим токен, сгенерированный пользователем/входом в систему, в заголовок запроса интерфейса hello, мы сможем получить к нему обычный доступ.
Тогда цель нашего набора фильтров достигнута (получение токенов, анализ токенов и сохранение их в SecurityContextHolder)
На этом этапе нам проще выйти из системы. Нам нужно только удалить соответствующие данные в Redis. Когда мы позже принесем токен для доступа, соответствующая информация о пользователе в Redis будет получена в нашем пользовательском фильтре. Вы не можете его получить, это означает, что вы не авторизованы.
Мы несем этот токен для доступа к интерфейсу /user/logout.
Затем реализована функция выхода из системы.
Эту статью узнали из третьего обновления станции b! ! !