SpringSecurity
b站
40.源码讲解部分说明_哔哩哔哩_bilibili
BV1mm4y1X7Hc
以下全为个人总结,不能代表官方,有错误还请指出(全部测试通过)(1刷视频)
1-简介
tip
接下来的所有类不会包含import信息,请在设置里开启自动导包功能
java框架
身份验证,授权,防止攻击
基于过滤器链,可集成到Spring应用
举例
认证:回家时,房子里的人问你是谁,得知你是其中的成员后则可进入
授权:进到屋子里,只有权限躺地上睡觉,没有其他使用电视,wifi的权限
2-入门案例
新建空项目-新建maven模块
加入依赖+boot父工程
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.5.0</version>
</parent>
<dependencies>
<!--security依赖项目(重点)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
创建启动类
@SpringBootApplication
public class SecurityApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SecurityApplication.class);
}
}
创建测试类Hello
@RestController
public class HelloController {
@GetMapping
public String hello(){
return "Hello World!";
}
}
浏览器访问测试
http://localhost:8080/
自动跳转登录页面
用户名为user
密码为
注销url
Confirm Log Out?
3-认证
3.1-登录校验流程
-
输入密码账号
-
后端拿用户名和密码比对数据库
-
正确则生成token给前端
-
后续前端所有请求头携带该token
-
后端根据token判断是否有权限,有放行响应结果给前端,没有则拦截
输密码-查库-造token-每次访问查token
3.2-完整流程
过滤器链
认证——UsernamePasswordAuthenticationFilter
授权——FilterSecurityInterceptor
异常处理——Exception…
默认登录/注销页
查看springsecurity默认过滤器链
使用idea评估器查看容器内的bean
上到下依次执行
3.3-案例认证流程
以下的UsernamePasswordAuthenticationFilter统称为UPAF
Authentication统称为A
upaf实现a对象 目前拥有 用户名和密码信息
调用 a上的方法认证,最后到loaduser…,根据用户名从内存或是库里获取出来
获取到的用户信息userdetails也就是loaduser…方法的放回值,将前台输入的密码和根据用户名查到的密码通过encoder编码后进行比对
正确则给a 增加权限信息,目前拥有用户名和密码信息和权限信息
并将该结果对象作为认证方法的返回值返回给upaf
前台输入的账号密码
调用方法开始认证
库里根据账号查出密码
编码后比对,正确则添加权限信息并返回对象(uname,pwd,roles)
对象存到security容器里 (securityContextHolder…)
如何DIY?
还记得我们一开始访问的登陆页面吗,使用的密码是随机生成并存到内存里的
那是由于上述流程图的inMemory…为默认过滤器的验证接口UserDetailsService的实现类,且底层自动写好了
如果我们想要从数据库查询呢?自己重写呗
eg—— xxxx implement UserDetailsService
4-数据库验证案例实战
4.1-思路分析
登录
- 自定义登录接口
由于校验后返回的a对象由upaf存入security容器中,但是这个类没有存token的方法,因此我们要自己写一个自定义接口来生成token并且返回
但是token只包含了userid,如何查询roles权限信息呢?查库?那每次访问鉴权都得查库就太费性能了,第一次登录后生成jwt的同时给他整到redis里
k:userID
v:userinfo(roles…name…sex…)
- 生成jwt
- 用户信息存redis里
- 自定义登录校验类UserDetailsService
- 查库
校验
①定义jwt过滤器
- 拿token
- 解析userid
- 从redis拿用户信息(拿roles然后再由security校验)
4.2-java准备工作
新建模块-启动类-依赖-hello测试接口,和上面操作一样
工具依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>securityTokenDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Archetype - securityTokenDemo</name>
<url>http://maven.apache.org</url>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.5.0</version>
</parent>
<dependencies>
<!-- Spring Boot 安全功能的starter包,用于web应用的安全控制 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Web功能的starter包,提供web应用的基本功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok,提供简单的代码生成工具,减少样板代码,设置为可选依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot的测试starter包,用于单元测试和集成测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security的测试包,用于安全测试 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Redis的starter包,用于集成Redis作为缓存或持久化方案 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- FastJSON,一个Java语言编写的高性能功能完备的JSON库 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!-- JWT(JSON Web Token)的库,用于生成和解析JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- JAXB API,用于XML和Java对象之间的绑定 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!--mybatis-plus的springboot支持-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot的测试starter包,重复项,可能用于不同目的 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
redis配置类
package com.example.securitytest.config;
import com.example.securitytest.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来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
fastjson工具类
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);
}
}
jwt工具类
package com.example.securitytest.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;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "sangeng";
public static String getUUID() {
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
*
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
*
* @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("sg") // 签发者
.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 ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg ";
Claims claims = parseJWT(token);
System.out.println(claims);
}
/**
* 生成加密后的秘钥 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.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();
}
}
rediscache工具类
package com.example.securitytest.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);
}
}
响应渲染类
package com.example.securitytest.utils;
import jakarta.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;
}
}
User实体类
package com.example.securitytest.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.io.Serializable;
/**
* <p>
* 用户表
* </p>
*
* @author 哈纳桑
* @since 2024-05-07
*/
@TableName("sys_user")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 用户类型:0代表普通用户,1代表管理员
*/
private String type;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer delFlag;
}
4.3-持久层准备工作
建表
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 '密码',
`type` char(1) DEFAULT '0' COMMENT '用户类型:0代表普通用户,1代表管理员',
`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 '头像',
`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 '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14787164048663 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='用户表'
mp依赖(前面引入过了)
<!-- MyBatis Plus的Spring Boot starter,用于简化MyBatis的使用 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL连接器,用于连接和操作MySQL数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
yml配置文件
spring:
application:
name: SecurityTest
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sg_security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: qq1664546939
data:
redis:
host: localhost
port: 6379
password: 123456
database: 10
server:
port: 8888
mapper接口
package com.example.securitytest.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.securitytest.domain.User;
public interface UserMapper extends BaseMapper<User> {}
实体类修改
类名指定表,指定主键
@TableName("sys_user")
@TableId(value = "id", type = IdType.*AUTO*)
启动类mapper扫描
@MapperScan("zww.mapper")
单元测试类
新建test/java文件目录,建包zww
package com.zww;
import com.zww.domain.User;
import com.zww.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
/**
* @BelongsProject: SpringSecurity
* @BelongsPackage: zww
* @Author: Zww
* @CreateTime: 2024-08-05 12:27
* @Description: TODO
* @Version: 1.0
*/
@SpringBootTest
public class MapperTest {
@Resource
private UserMapper userMapper;
@Test
void test() {
List<User> users = userMapper.selectList(null);
System.out.println(users);
}
}
4.4业务层
实现UserDetails类
既要有userdetails上的玩意还要有自己的user内容那就将user对象作为属性给到userdetials
到时候security就会调用这个对象的getUsername和getPassword所以这里我们给重写的方法返回我们自己的user.getUsername/password
package com.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;
/**
* @BelongsProject: SpringSecurity
* @BelongsPackage: com.example.domain
* @Author: Zww
* @CreateTime: 2024-08-05 15:39
* @Description: TODO
* @Version: 1.0
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
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 false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
自定义登录实现类
最后user作为构造函数的参数传入,new出来loginUser也就是userdetails传给接口调用者 Userdetails ud=xxxx.authenticate()
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(wrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名或者密码错误");
}
return new LoginUser(user);
}
}
4.5测试
由于默认的密码校验需要 前缀有{noop}(noop明文)标识密码类型,否则登录失败
出现用户已锁定,已失效等问题是因为实现userdetails方法里的权限等返回值默认为false,全改为true即可
4.6加密解密
默认使用passwordencoder要求数据库中的密码格式为:{id}xxx以id判断加密方式,一般不采用,这里我们返回一个新的加密对象
继承配置类websecurityconfigurerAdaper
@Configuration //配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
先用明文生成一串加密后的密码,然后登录测试
@Test
void createPwd(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123");
System.out.println(encode);
}
这串密码给他沾到数据库里去
一样的明文生成不一样的密文,加盐了——————
密码校验
@Test
void checkPwd(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
boolean matches = bCryptPasswordEncoder.matches("123", "$2a$10$XPrtEs74Qw.MK9JUuazRj.omKpRpp7Ir9QxrQGE/0ptJH.ZHv.1km");
System.out.println(matches);
}
业务代码校验
不new Bcript对象了直接注入解密器
@Test
void checkPwd2(){
boolean matches=passwordEncoder.matches("123", "$2a$10$XPrtEs74Qw.MK9JUuazRj.omKpRpp7Ir9QxrQGE/0ptJH.ZHv.1km");
System.out.println(matches);
}
4.7登录接口
校验成功后我们需要生成一个jwt
jwt工具类及demo
package com.example.util;
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;
/**
* JWT工具类
*/
public class JwtUtil {
// 有效期为
public static final Long JWT_TTL = 60 * 60 * 1000L;// 60 * 60 *1000 一个小时
// 设置秘钥明文
public static final String JWT_KEY = "sangeng";
public static String getUUID() {
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
*
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
*
* @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("sg") // 签发者
.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 {
System.out.println("制作jwt中...");
String jwt = createJWT("123");
System.out.println(jwt);
System.out.println("解析jwt中...");
String token = jwt;
Claims claims = parseJWT(token);
System.out.println(claims.getSubject());
}
/**
* 生成加密后的秘钥 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.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();
}
}
运行main方法得到结果
具体思路
通过aManager的authenticate进行用户认证,需要先在security配置类把aManager注入security容器
authenticate方法认证成功后 生成jwt给用户一份,给redis一份,id作为key
定义结果类
package com.example.domain;// package com.zww.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;
}
}
定义service和impl
service
还记得开始那幅图吗,我们前面走了后面的路线,现在开始从前面的接口开始继续
用户名和密码需要封装在authent里… 这里使用upat 作为容器
配置文件类
加入authenticationManager
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
security配置类重写放行login的请求
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
//关闭csrf
csrf().disable()
//不通过session获取securityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//登录接口放行
.antMatchers("/user/login").anonymous()
//其余全部鉴权认证
.anyRequest().authenticated();
}
impl
@Service
public class LoginServiceImpl implements LoginService {
@Resource
AuthenticationManager authenticationManager;
@Override
public ResponseResult login(User user) {
// 用户名和密码封装到验证容器对象里
UsernamePasswordAuthenticationToken upat=
new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
// 开始验证
Authentication authenticate = authenticationManager.authenticate(upat);
//如果验证结果也就是返回值 也就是在数据库里查得到,那么就验证通过了
if (Objects.isNull(authenticate)){
throw new RuntimeException("登陆失败");
}
return null;
}
}
总结
- 注入amanager
- 调用authenticate方法,传入 实现类upat实现了authentication
- 调用自定义userdetailsServiceImpl实现类方法,返回从数据库查询到的Userdetails对象,使用loginUser实现类并传入自己的user类
- 接口类判断返回的userdetails是否为空,不为空则拿userid造token,然后将userdetails存redis,后续根据id查询权限
- 返回携带token的map给前端
拿upat认证对象调认证方法,根据返回值认证,造token,前端拿一份,redis拿一份认证对象(含roles后续鉴权)
认证,生成令牌,返回前端和redis
4.8jwt验证
过滤器获取token-解析token-拿token查redis-存security容器-放行
jwt过滤器类
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
filterChain.doFilter(request, response);
// 如果 token 为空,则直接放行请求,让后续的过滤器链继续处理请求。这样做的原因是:
// 登录接口给security认证
// 访问控制: 在某些场景下,并不是所有的请求都需要经过身份验证或授权。
// 比如一些静态资源、登录接口等,可以允许任何用户访问。对于这些不需要验证的请求,
// 直接放行可以提高系统的灵活性和性能。
// 过滤器链执行顺序: 过滤器链是一系列过滤器按照特定顺序执行的机制。
// 如果不在当前过滤器中直接返回,后续的过滤器链会再次进入当前过滤器,造成重复执行。
// 这可能会导致一些不必要的性能开销或逻辑错误。
return;
}
// 根据token获取userid
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// 制作redisKey
String rkey = "login:" + userId;
// 查询到用户信息
// 由于getObject封装使用了泛型,不用强转
LoginUser loginUser = redisCache.getCacheObject(rkey);
if (Objects.isNull(loginUser)){
//有token但是查不到,说明token在redis里过期了
throw new RuntimeException("您的登录状态已过期,请重新登录");
}
//存入security容器,以authenticate类型,已认证状态设置为null先
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
由于是先进行token校验,然后redis获取loginuser信息,而非直接查询数据库,因该在security链之前,在配置类加入过滤器,并设置在security配置类之前
security的configure应用jwt过滤器
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
// 关闭csrf
csrf().disable()
// 不通过session获取securityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 登录接口放行
.antMatchers("/user/login").anonymous()
// 其余全部鉴权认证
.anyRequest().authenticated()
.and()
// 异常处理配置
.exceptionHandling()
// 认证异常处理
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(403);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new ResponseResult(403, "error")));
})
// 授权异常处理
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(403);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new ResponseResult(403, "error")));
});
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
测试访问
无token
登录后拿token放请求头在请求的结果为
4.9退出接口
so easy, 删redis即可,然后后面每次拿的token userid查token查不到就抛异常了
接口,service,实现类一气呵成
@Override
public ResponseResult logout() {
// jwt过滤器在请求过来的时候给security容器设置了authentication,这里我们直接拿就行
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userId = loginUser.getUser().getId();
String rkey = "login:" + userId;
redisCache.deleteObject(rkey);
return new ResponseResult(200, "注销成功");
}
测试
再来访问其他接口试试看
5-认证配置详解
httpsecurity上的各种接口链式调用
anonymous和permitAll的区别
permitall是所有都能访问,anonymous是未认证可以访问,认证了之后就不能访问
6-授权
6.1实现步骤
security配置类加注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
接口上加注解
@PreAuthorize("hasAuthority('test')")
自定义实现类添加权限列表
List<String> permission = new ArrayList<>(Arrays.asList("test","admin"));
return new LoginUser(user,permission);
loginUser类加权限属性
并完善实现类getAuthorities的逻辑,同时新增一个user,permission的构造函数,删除@AllArgu全参构造注解
package com.example.domain;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* @BelongsProject: SpringSecurity
* @BelongsPackage: com.example.domain
* @Author: Zww
* @CreateTime: 2024-08-05 15:39
* @Description: TODO
* @Version: 1.0
*/
@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
private User user;
private List<String> permission;
public LoginUser(User user, List<String> permission) {
this.user = user;
this.permission = permission;
}
//序列化关闭,否则序列化到redis里抛异常,为了安全考虑SimpleGrantedAuthority类型无法存储?
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 使用GrantedAuthority的实现ctrl+alt+左键查看,
// 如果每次get鉴权都得处理成集合那就太麻烦了,提升为成员变量永存,每次get进行判断即可
if (authorities!=null){
return authorities;
}
authorities = permission.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
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;
}
}
JWT过滤器设置权限
最后访问hello接口如果通过即可
6.2数据库查询权限信息
RBAC权限模型
Role Based Access Control角色权限控制模型
用户-角色(权限集合)-权限
用户有什么角色,什么角色对应什么权限,中间建立桥梁表
多对多建立关联表
所有表创建
CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
`parent_id` bigint DEFAULT '0' COMMENT '父菜单ID',
`order_num` int DEFAULT '0' COMMENT '显示顺序',
`path` varchar(200) DEFAULT '' COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`is_frame` int DEFAULT '1' COMMENT '是否为外链(0是 1否)',
`menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT '' COMMENT '备注',
`del_flag` char(1) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2034 DEFAULT CHARSET=utf8mb3 COMMENT='菜单权限表'
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(30) NOT NULL COMMENT '角色名称',
`role_key` varchar(100) NOT NULL COMMENT '角色权限字符串',
`role_sort` int NOT NULL COMMENT '显示顺序',
`status` char(1) NOT NULL COMMENT '角色状态(0正常 1停用)',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`create_by` bigint DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb3 COMMENT='角色信息表'
CREATE TABLE `sys_role_menu` (
`role_id` bigint NOT NULL COMMENT '角色ID',
`menu_id` bigint NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='角色和菜单关联表'
CREATE TABLE `sys_user_role` (
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='用户和角色关联表'
内容
SQL语句
-
根据user role表左连接role表,条件为roleid相等 这样就可以获取用户对应的所有角色
-
根据用户角色表在左连接menu_role表,条件为roleid相等,这样就可以获取用户的角色包含所有的menuId
-
根据menuid左连接menu表,条件为桥梁menuId,这样就可以获取用户的所有角色对应的所有菜单权限 perms,
-
select时DISTINCT去重,因为不同角色可能含有相同的菜单权限,比如老板和员工都有资格查看当前菜品的权限
中间桥梁为roleid,将user和menu权限连接
SELECT DISTINCT perms
FROM sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id`=rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id`=rm.`menu_id`
WHERE user_id=1
AND r.`status`=0
AND m.`status`=0
MENU持久层–实体,mapper,mapperxml
实体
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_menu")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 菜单ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 菜单名称
*/
private String menuName;
/**
* 父菜单ID
*/
private Long parentId;
/**
* 显示顺序
*/
private Integer orderNum;
/**
* 路由地址
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 是否为外链(0是 1否)
*/
private Integer isFrame;
/**
* 菜单类型(M目录 C菜单 F按钮)
*/
private String menuType;
/**
* 菜单状态(0显示 1隐藏)
*/
private String visible;
/**
* 菜单状态(0正常 1停用)
*/
private String status;
/**
* 权限标识
*/
private String perms;
/**
* 菜单图标
*/
private String icon;
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 备注
*/
private String remark;
private String delFlag;
}
小技巧,装mybatisX插件后对着mapper文件ctrl+回车 可以快速生成xml文件
mapper
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectPermsByUserId(Long userId);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.mapper.MenuMapper">
<select id="selectPermsByUserId" resultType="com.example.domain.Menu">
SELECT DISTINCT m.perms
FROM sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id`=rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id`=rm.`menu_id`
WHERE user_id=#{userId}
AND r.`status`=0
AND m.`status`=0
</select>
</mapper>
yml指定xml路径
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
测试类测试
修改原来写死的地方
测试从数据库查到的权限能否访问
7-异常信息自定义处理
默认的认证和授权异常为authenticationException和accessdeniedException
作为形参调用各自的接口方法处理异常
认证-authenticationEntryPoint
授权-accessdeniedHandler
而者两个接口的默认实现好像是空吧,因为我们前面没权限他返回值好像啥也没有
我们自己重新建两个实现类实现这两个接口后续将其配置到security配置类后出现异常就会应用我们自己写的实现类里的处理方法
认证
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED, "用户名或密码错误,请重新输入");
String jsonString = JSON.toJSONString(result);
// web响应渲染,指定了响应状态码,json格式,编码utf,getWriter写内容
WebUtils.renderString(response, jsonString);
}
}
授权
差不多其实,就状态码和msg变了而已
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN, "无权限访问");
String jsonString = JSON.toJSONString(result);
// web响应渲染,指定了响应状态码,json格式,编码utf,getWriter写内容
WebUtils.renderString(response, jsonString);
}
}
配置到配置类里
注入
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
启动测试
测试没权限
测试没认证
8-跨域
浏览器屁事多,有洁癖原则,同源策略, 协议http主机地址www.www.com/192…端口80 要同个格式同个端口,否则报错
但是前后端分离 协议端口主机地址包不一样的
解决访问为,后端处理,允许跨域访问
跨域配置类
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
security配置允许跨域
如果要测试可以随便找个前端项目发请求,推荐vue,搞个axios.post一下
用postman测没用的,因为他不是浏览器没有洁癖(同源策略)
9-权限校验
框架提供
源码解析
本质就是将你在接口上定义的权限要求和查询到拥有的权限列表集合作对比
如果你拥有的权限 .contain在需要的权限里,符合要求了,则执行
这里还有一个默认前缀为空的参数,会对其和权限名做一个拼接,下面是图例介绍
首先将注解里的权限要求传入方法…
hasAnyAuthority
传入多个权限要求,只要你有其中一个就可以进入,类似于一个房间,你如果2米9可以进入,你如果喜欢唱歌也可以进入
同时满足就还是用hasAuthority加and and and即可,不过不建议这样,一个菜单对应一个权限有要求就行
hasRole
默认前缀ROLE_,所以我们在数据库里存的权限标识符也得有前缀
hasAnyRole
同hasAnyAuthen一样
自定义权限校验
创建自定义类,写方法,@Component指定名字(“ex”)
@Component("ex")
public class ZwwExpressionRoot {
public boolean hasAuthority(String authority){
//获取当前用户权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser principal = (LoginUser) authentication.getPrincipal();
//前面在自定义认证实现类已经设置了permission
List<String> permission = principal.getPermission();
return permission.contains(authority);
}
}
可以自行断点测试是否走你自定义的那个方法
基于配置类配置权限
其他hasRole都有,调用就完事了
10-CSRF
介绍
你登录了淘宝网站,没退出,保留在淘宝的登录记录(cookie等)
访问了病毒网站,做的和淘宝网站一模一样,点击了按钮,他拿你的身份给淘宝下了单,买了东西,或者转账等敏感操作, 但是这不是你干的,这个行为是他伪装成你做出来的,所以称之为 跨站请求伪造cross-site request forgery
其依靠cookie中携带的认证信息,因为发请求会把cookie一起扔过去,但是我们使用的是在请求头里放token进行认证的呀,这样一来,就不用使用csrf了直接关闭就行
11-自定义认证成功处理器
用之前的quickstart项目
和前面的有什么区别吗?
前面的使用了自定义处理类authmanager.authenticate方法 也就是自定义login接口的实现查数据库
这个是表单处理,使用usernamepasswordfilter处理
成功处理类
@Component
public class ZwwAuthenticationSuccess implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("认证成功咯");
}
}
配置类
这是默认的
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
ZwwAuthenticationSuccess zwwAuthenticationSuccess;
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用自定义的登录成功处理类
http.formLogin().successHandler(zwwAuthenticationSuccess);
//重写了原来默认的配置清空,需要对所有请求继续拦截
http.authorizeRequests().anyRequest().authenticated();
}
}
测试
失败处理类
@Component
public class ZwwAuthenticationFail implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("认证失败了");
}
}
配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
ZwwAuthenticationSuccess zwwAuthenticationSuccess;
@Autowired
ZwwAuthenticationFail zwwAuthenticationFail;
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用自定义的登录成功处理类
http.formLogin()
.successHandler(zwwAuthenticationSuccess)
.failureHandler(zwwAuthenticationFail);
//重写了原来默认的配置清空,需要对所有请求继续拦截
http.authorizeRequests().anyRequest().authenticated();
}
}
自行测试
注销成功处理器
重复操作,实现类,注入,configure里添加
自定义类
@Component
public class ZwwLogoutSuccess implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("注销成功");
}
}
配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
ZwwAuthenticationSuccess zwwAuthenticationSuccess;
@Autowired
ZwwAuthenticationFail zwwAuthenticationFail;
@Autowired
ZwwLogoutSuccess zwwLogoutSuccess;
@Override
protected void configure(HttpSecurity http) throws Exception {
//使用自定义的登录成功处理类
http.formLogin()
.successHandler(zwwAuthenticationSuccess)
.failureHandler(zwwAuthenticationFail);
http.logout().logoutSuccessHandler(zwwLogoutSuccess);
//重写了原来默认的配置清空,需要对所有请求继续拦截
http.authorizeRequests().anyRequest().authenticated();
}
}
自行测试~
12-其他认证方案畅想
以下为全文回顾,懒得看可跳过
之前方案没用user…filter而是自己写了自定义实现类,调用ProvideManager,authenticationManager实现类上的方法,然后就跑到我们
自己实现且覆盖默认的userdetailsImpl类里面查库,然后查权限,最后返回的值类型为UserDetails实体,我们就用一个LoginUser去实现,然后里面含有我们自己diy的user,同时对其getAuthorities方法重写加工了从数据库查到的权限表,等一些其他类的重写,最后将LoginUser实例形参为user,和权限表permission,然后我们此时还想制作token是吧,用了接口生成了token然后一份放redis一份做了map给前端保存,每次过来请求头携带,然后且进行redis查询,获取到权限列表进行鉴权,就不用老查库获取permission权限集合了,其实我这里有个疑问,如果能存在token里就不用放redis了,但是放redis还有一个好处就是,如果想封禁用户,由于token时间是固定的,我们不能强行不让用户登录,但是可以将redis缓存删掉,登录进来redis查不到就踢出也可以说是拒绝登录也可以说是封禁了,这过程我们还搞了异常处理,开启跨域,还有权限设置,和ROLE BASED ACCESS CONTROL 权限控制模型, 搞了五张表,左连接多表查询最后千辛万苦计算出userid为1的靓仔拥有的权限集合,然后搁接口上进行hasAuthentication判断contains是否匹配,然后放行,不过这里的难点是jwt过滤器,一开始没token直接放行,说是有可能请求登录接口可以通过没疑问,但是三更说不return他会绕一圈跑回来继续判断token,有点迷,这里还有一个是将jwt的过滤器定义在了userfilter之前…先鉴token,token-》userid查redis然后根据getAuthorities拿到的权限列表后filterChain.doFilter(request, response);,跑到接口那@PreAuthorize(“@ex.hasAuthority(‘system:dept:list’)”)进行下一层权限验证过滤器链校验,最后再执行逻辑,这一段估计有点难记住,先jwt过滤器,然后再权限校验链
其他认证方案
重写userpasswordauthenticationFIlter ,然后还是自定义实现userdetailImpl查库,最后制作token返回前端,不过这一步我们一开始是用login接口实现,还可以把它放在认证成功处理,也就是最后面提到的那个AuthenticationSuccessHandler
如果再验证前加个验证码的校验,就把他写在userpasswordauthenticationFIlter 前。配置类里
13-源码讲解
投票过50…