SpringSecurity简单入门
1、SpringSecurity简介
Spring Security
是 Spring
家族中的一个安全管理框架,相比于另外一个安全框架 Shiro
,它提供了更丰富
的功能,社区资源也比 Shiro
丰富。
一般来说中大型的项目都是使用 SpringSecurity
来做安全框架,小项目使用 Shiro
的比较多,因为相比于
SpringSecurity
,Shiro
上手更加的简单。
一般 Web
应用需要进行认证和授权:
-
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。
-
授权:经过认证后判断当前用户是否有权限进行某个操作。
认证和授权也是 SpringSecurity
作为安全框架的核心功能。
2、快速入门
2.1 新建测试控制器
package com.example.springsecuritydemo1.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author test
*/
@RestController
public class HelloWorldController {
@GetMapping("hello/world")
public String helloWorld() {
return "hello world!";
}
}
2.2 pom依赖
在 SpringBoot
项目中使用 SpringSecurity
我们只需要引入依赖即可实现入门案例。
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>SpringSecurityDemo1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringSecurityDemo1</name>
<description>SpringSecurityDemo1</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 引入spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.3 使用
引入依赖后我们在尝试去访问接口就会自动跳转到一个 SpringSecurity
的默认登陆页面,默认用户名是
user
,密码会输出在控制台,必须登陆之后才能对接口进行访问。
# 控制台输出的密码
Using generated security password: 83cf6d36-d553-4c52-bd4b-f5a107a0a945
我们访问之前的接口需要输入用户名和密码才可以访问:
访问接口:
3、认证
3.1 登录校验流程
3.2 原理初探
想要知道如何实现自己的登陆流程就必须要先知道入门案例中 SpringSecurity
的流程。
3.2.1 SpringSecurity完整流程
SpringSecurity
的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,这里我们可以看看入门案
例中的过滤器。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
-
UsernamePasswordAuthenticationFilter
:负责处理我们在登陆页面填写了用户名密码后的登陆请求,入门案例的认证工作主要有它负责。
-
ExceptionTranslationFilter
:处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
。 -
FilterSecuritylnterceptor
:负责权限校验的过滤器。
我们可以通过 Debug
查看当前系统中 SpringSecurity
过滤器链中有哪些过滤器及它们的顺序。
3.2.2 认证流程详解
概念速查:
-
Authentication
接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。 -
AuthenticationManager
接口:定义了认证Authentication
的方法 。 -
UserDetailsService
接口:加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。
-
UserDetails
接口:提供核心用户信息,通过UserDetailsService
根据用户名获取处理的用户信息要封装成
UserDetails
对象返回,然后将这些信息封装到Authentication
对象中。
3.3 解决问题和思路分析
登录:
①、自定义登录接口
-
调用
ProviderManager
的方法进行认证,如果认证通过生成jwt
-
把用户信息存入
redis
中
②、自定义 UserDetailsService
- 在这个实现类中去查询数据库
校验:
①、定义 Jwt
认证过滤器
- 获取
token
- 解析
token
获取其中的userid
- 从
redis
中获取用户信息 - 存入
SecurityContextHolder
4、认证案例
4.1 pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>SpringSecurityDemo2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringSecurityDemo2</name>
<description>SpringSecurityDemo2</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 引入spring security -->
<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.83</version>
</dependency>
<!-- 引入jwt依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 引入lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.2 Redis相关配置
# Redis配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1ms
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000ms
4.3 Redis配置类
package com.example.springsecuritydemo2.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author test
*/
@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 定义Jackson2JsonRedisSerializer序列化对象
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会报异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 创建RedisTemplate<String, Object>对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer stringSerial = new StringRedisSerializer();
// redis key 序列化方式使用stringSerial
template.setKeySerializer(stringSerial);
// redis value 序列化方式使用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// redis hash key 序列化方式使用stringSerial
template.setHashKeySerializer(stringSerial);
// redis hash value 序列化方式使用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4.4 Redis工具类
package com.example.springsecuritydemo2.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* @author test
*/
@Component
public class RedisUtils {
private RedisTemplate<String, Object> redisTemplate;
@Autowired
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 删除缓存
*
* @param key 可以传一个值或多个
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(Arrays.asList(key));
}
}
}
}
4.5 Jwt工具类
package com.example.springsecuritydemo2.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author test
*/
@Component
public class JwtTokenUtil {
/**
* 荷载claim的名称
*/
private static final String CLAIM_KEY_USERNAME = "sub";
/**
* 荷载的创建时间
*/
private static final String CLAIM_KEY_CREATED = "created";
/**
* jwt令牌的秘钥
*/
private final String secret = "yeb-secret";
/**
* jwt的实效时间
*/
private final Long expiration = 604800L;
/**
* 根据用户信息生成token
*
* @param username 用户名
* @return String
*/
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, username);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 根据荷载生成JWTToken
*
* @param claims 生成token的信息
* @return String
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder().setClaims(claims).setExpiration(generateExpiration()).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 生成token实效时间
*
* @return Date
*/
private Date generateExpiration() {
// 当前时间+配置时间
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 根据token获取荷载
*
* @param token 传入的token
* @return Claims
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 从token获取用户信息
*
* @param token 传入的token
* @return String
*/
public String getUserNameFromToken(String token) {
String username;
Claims claims = getClaimsFromToken(token);
try {
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 刷新token
*
* @param token 传入的token
* @return String
*/
public String refreshToken(String token) {
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 判断token是否可以被刷新
*
* @param token 传入的token
* @return Boolean
*/
public Boolean canRefresh(String token) {
return !isTokenExpired(token);
}
/**
* 判断token是否失效
*
* @param token 传入的token
* @return boolean
*/
private boolean isTokenExpired(String token) {
Date expireDate = getExpiredDateFromToken(token);
//如果token有效的时间在当前时间之前就表示实效
return expireDate.before(new Date());
}
/**
* 从token中获取实效时间
*
* @param token 传入的token
* @return Date
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
}
4.6 Web工具类
package com.example.springsecuritydemo2.util;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author test
*/
public class WebUtil {
public static void renderString(HttpServletResponse response, String str) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().println(str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.7 响应类
package com.example.springsecuritydemo2.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author test
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseResult<T> {
private Integer code;
private String msg;
private T data;
}
4.8 数据库校验用户表
从之前的分析我们可以知道,我们可以自定义一个 UserDetailsService
,让 SpringSecurity
使用我们的
UserDetailsService
,我们自己的 UserDetailsService
可以从数据库中查询用户名和密码。
我们先创建一个用户表,建表语句如下:
CREATE TABLE `sys_user` (
`id` bigint(20) 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 '邮箱',
`phone_number` 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 '用户类型(0管理员,1普通用户)',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
INSERT INTO test.sys_user (user_name,nick_name,password,status,email,phone_number,sex,avatar,user_type,create_by,create_time,update_by,update_time,del_flag) VALUES
('tom','tom','$2a$10$M5W1uFxQW7jvWNEe9.Cs3.ENIQjy8P/.un3v8plySy.XdrY/mqRua','0',NULL,NULL,NULL,NULL,'1',NULL,NULL,NULL,NULL,0);
4.9 引入MybatisPlus和MySQL的依赖
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
4.10 配置数据库信息
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
4.11 定义Mapper接口
package com.example.springsecuritydemo2.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springsecuritydemo2.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* @author test
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
4.12 User实体类
package com.example.springsecuritydemo2.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* @author test
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@TableId
private Long id;
private String userName;
private String nickName;
private String password;
private String status;
private String email;
private String phoneNumber;
private String sex;
private String avatar;
private String userType;
private String createBy;
private Date createTime;
private Date updateTime;
private String delFlag;
}
4.13 配置Mapper扫描
package com.example.springsecuritydemo2;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author test
*/
@SpringBootApplication
@MapperScan("com.example.springsecuritydemo2.mapper")
public class SpringSecurityDemo2Application {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityDemo2Application.class, args);
}
}
4.14 测试Mapper是否能够正常使用
package com.example.springsecuritydemo2;
import com.example.springsecuritydemo2.entity.User;
import com.example.springsecuritydemo2.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class SpringSecurityDemo2ApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void testUserMapper() {
List<User> userList = userMapper.selectList(null);
System.out.println(userList);
}
}
4.15 核心代码实现
4.15.1 UserDetailsService接口重写
创建一个类实现 UserDetailsService
接口,重写其中的方法,用户名从数据库中查询用户信息。
package com.example.springsecuritydemo2.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.springsecuritydemo2.entity.LoginUser;
import com.example.springsecuritydemo2.entity.User;
import com.example.springsecuritydemo2.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
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;
/**
* @author test
*/
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private UserMapper userMapper;
@Autowired
public void setUserMapper(UserMapper userMapper){
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername");
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(lambdaQueryWrapper);
// 没有查询到用户抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误!");
}
// 把用户封装成UserDetails
return new LoginUser(user);
}
}
4.15.2 UserDetails实现重写
因为 UserDetailsService
方法的返回值是 UserDetails
类型,所以需要定义一个类,实现该接口,把用户信
息封装在其中。
package com.example.springsecuritydemo2.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Collection;
/**
* @author test
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class LoginUser implements UserDetails, Serializable {
private static final long serialVersionUID = 1L;
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}
。例如:
这样登陆的时候就可以用 tom
作为用户名,tom
作为密码来登陆了。
4.15.3 密码加密存储
实际项目中我们不会把密码明文存储在数据库中。
默认使用的 PasswordEncoder
要求数据库中的密码格式为 {id}password
,它会根据 id
去判断密码的加密方
式,但是我们一般不会采用这种方式,所以就需要替换 PasswordEncoder
。
package com.example.springsecuritydemo2;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootTest
class SpringSecurityDemo2ApplicationTests {
@Test
void testBCryptPasswordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String pass = bCryptPasswordEncoder.encode("tom");
// $2a$10$M5W1uFxQW7jvWNEe9.Cs3.ENIQjy8P/.un3v8plySy.XdrY/mqRua
System.out.println(pass);
boolean match = bCryptPasswordEncoder.matches("tom", "$2a$10$M5W1uFxQW7jvWNEe9.Cs3.ENIQjy8P/.un3v8plySy.XdrY/mqRua");
System.out.println(match);
}
}
我们一般使用 SpringSecurity
为我们提供的 BCryptPasswordEncoder
。
我们只需要使用把 BCryptPasswordEncoder
对象注入 Spring
容器中,SpringSecurity
就会使用该
PasswordEncoder
来进行密码校验。
我们可以定义一个 SpringSecurity
的配置类,SpringSecurity
要求这个配置类要继承
WebSecurityConfigurerAdapter
。
package com.example.springsecuritydemo2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @author test
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4.15.4 登陆接口
接下我们需要自定义登陆接口,然后让 SpringSecurity
对这个接口放行,让用户访问这个接口的时候不用登录
也能访问。
在接口中我们通过 AuthenticationManager
的 authenticate
方法来进行用户认证,所以需要在
SecurityConfig
中配置把 AuthenticationManager
注入容器。
package com.example.springsecuritydemo2.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @author test
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf
http.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 请求认证
.authorizeRequests()
// 对于登录接口,允许匿名访问(未登录情况下)
.antMatchers("/user/login").anonymous()
// 除上面的请求外其它请求都需要认证
.anyRequest().authenticated();
}
}
认证成功的话要生成一个 jwt
,放入响应中返回。并且为了让用户下回请求时能通过 jwt
识别出具体的是哪个用
户,我们需要把用户信息存入 redis
,可以把用户 username
作为 key
。
package com.example.springsecuritydemo2.controller;
import com.example.springsecuritydemo2.entity.ResponseResult;
import com.example.springsecuritydemo2.entity.User;
import com.example.springsecuritydemo2.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author test
*/
@RestController
public class LoginController {
private LoginService loginService;
@Autowired
public void setLoginService(LoginService loginService) {
this.loginService = loginService;
}
@PostMapping("user/login")
public ResponseResult<String> login(@RequestBody User user) {
return loginService.login(user);
}
}
package com.example.springsecuritydemo2.service;
import com.example.springsecuritydemo2.entity.ResponseResult;
import com.example.springsecuritydemo2.entity.User;
/**
* @author test
*/
public interface LoginService {
/**
* 登录
*
* @param user 用户信息
* @return ResponseResult<String>
*/
ResponseResult<String> login(User user);
}
package com.example.springsecuritydemo2.service.impl;
import com.example.springsecuritydemo2.entity.LoginUser;
import com.example.springsecuritydemo2.entity.ResponseResult;
import com.example.springsecuritydemo2.entity.User;
import com.example.springsecuritydemo2.service.LoginService;
import com.example.springsecuritydemo2.util.JwtTokenUtil;
import com.example.springsecuritydemo2.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* @author test
*/
@Slf4j
@Service
public class LoginServiceImpl implements LoginService {
private AuthenticationManager authenticationManager;
@Autowired
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
private JwtTokenUtil jwtTokenUtil;
@Autowired
public void setJwtTokenUtil(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
private RedisUtils redisUtils;
@Autowired
public void setRedisUtils(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Override
public ResponseResult<String> login(User user) {
log.info("login");
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
log.info("authenticate");
// 会调用loadUserByUsername()方法
Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if (Objects.isNull(authentication)) {
throw new RuntimeException("登录失败,用户名或密码错误!");
}
// 获取用户的信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 存入Redis中
boolean success = redisUtils.set("login:" + loginUser.getUser().getUserName(), loginUser);
if (!success) {
throw new RuntimeException("存入Redis出错!");
}
// 生成token
String token = jwtTokenUtil.generateToken(loginUser.getUser().getUserName());
return new ResponseResult<>(200, "登录成功", token);
}
}
4.15.5 认证过滤器
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的 token
,对 token
进行解析取出其中的 userid
。
使用 userid
去 redis
中获取对应的 LoginUser
对象,然后封装 Authentication
对象存入
SecurityContextHolder
。
package com.example.springsecuritydemo2.filter;
import com.example.springsecuritydemo2.entity.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* @author test
*/
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private JwtTokenUtil jwtTokenUtil;
@Autowired
public void setJwtTokenUtil(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
private RedisUtils redisUtils;
@Autowired
public void setRedisUtils(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
log.info("doFilterInternal");
// 获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
// 放行,后面会有别的过滤器处理
filterChain.doFilter(request, response);
return;
}
// 解析token
String username = jwtTokenUtil.getUserNameFromToken(token);
if (Objects.isNull(username)) {
throw new RuntimeException("token非法!");
}
// 从Redis中获取用户信息
LoginUser loginUser = (LoginUser) redisUtils.get("login:" + username);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录!");
}
// 存入SecurityContextHolder
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}
package com.example.springsecuritydemo2.config;
import com.example.springsecuritydemo2.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author test
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
public void setJwtAuthenticationTokenFilter(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf
http.csrf().disable()
// 不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 请求认证
.authorizeRequests()
// 对于登录接口,允许匿名访问(未登录情况下)
.antMatchers("/user/login").anonymous()
// 除上面的请求外其它请求都需要认证
.anyRequest().authenticated();
// 用户登录之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
4.15.6 退出登陆
我们只需要定义一个登陆接口,然后获取 SecurityContextHolder
中的认证信息,删除 redis
中对应的数据即
可。
package com.example.springsecuritydemo2.controller;
import com.example.springsecuritydemo2.entity.ResponseResult;
import com.example.springsecuritydemo2.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author test
*/
@RestController
public class LoginController {
private LoginService loginService;
@Autowired
public void setLoginService(LoginService loginService) {
this.loginService = loginService;
}
@GetMapping("user/logout")
public ResponseResult<String> logout() {
return loginService.logout();
}
}
package com.example.springsecuritydemo2.service;
import com.example.springsecuritydemo2.entity.ResponseResult;
/**
* @author test
*/
public interface LoginService {
/**
* 退出登录
*
* @return ResponseResult<String>
*/
ResponseResult<String> logout();
}
package com.example.springsecuritydemo2.service.impl;
import com.example.springsecuritydemo2.entity.LoginUser;
import com.example.springsecuritydemo2.entity.ResponseResult;
import com.example.springsecuritydemo2.service.LoginService;
import com.example.springsecuritydemo2.util.JwtTokenUtil;
import com.example.springsecuritydemo2.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
/**
* @author test
*/
@Slf4j
@Service
public class LoginServiceImpl implements LoginService {
private AuthenticationManager authenticationManager;
@Autowired
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
private JwtTokenUtil jwtTokenUtil;
@Autowired
public void setJwtTokenUtil(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
private RedisUtils redisUtils;
@Autowired
public void setRedisUtils(RedisUtils redisUtils) {
this.redisUtils = redisUtils;
}
@Override
public ResponseResult<String> logout() {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) usernamePasswordAuthenticationToken.getPrincipal();
String username = loginUser.getUser().getUserName();
redisUtils.del("login:" + username);
return new ResponseResult<>(200, "退出登录成功", null);
}
}
5、授权
5.1 权限系统的作用
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用
添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍
信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能,这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮,因为如果只是这样,如果有人知道了对应功
能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须基于所需权限才能进行相应的
操作。
5.2 授权基本流程
在 SpringSecurity
中,会使用默认的 FilterSecuritylnterceptor
来进行权限校验。
在 FilterSecuritylnterceptor
中会从 SecurityContextHolder
获取其中的 Authentication
,然后获取其
中的权限信息,判断当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入 Authentication
,然后设置我们的资源所需要的权
限即可。
5.3 授权实现
5.3.1 限制访问资源所需权限
SpringSecurity
为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式,我们可以使用注
解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {}
然后就可以使用对应的注解:
@PreAuthorize("hasAuthority('test')")
@GetMapping("hello/world")
public String helloWorld() {
return "hello world!";
}
5.3.2 封装权限信息
我们前面在写 UserDetailsServicelmpl
的时候说过,在查询出用户后还要获取对应的权限信息,封装到
UserDetails
中返回。
我们先直接把权限信息写死封装到 UserDetails
中进行测试。
我们之前定义了 UserDetails
的实现类 LoginUser
,想要让其能封装权限信息就要对其进行修改。
5.4 RBAC权限模型
RBAC
权限模型(Role-Based Access Control),即:基于角色的权限控制。这是目前最常被开发者使用也是相对易
用、通用权限模型。
5.5 RABC数据表
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`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(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(O未删除,1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常,1停用)',
`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(200) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
CREATE TABLE `sys_user` (
`id` bigint(20) 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 '邮箱',
`phone_number` 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 '用户类型(0管理员,1普通用户)',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
CREATE TABLE `sys_role_menu` (
`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `sys_user_role` (
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO test.sys_menu (menu_name,`path`,component,visible,status,perms,icon,create_by,create_time,update_by,update_time,del_flag,remark) VALUES
('管理',NULL,NULL,'0','0','system:admin','#',NULL,NULL,NULL,NULL,0,NULL),
('测试',NULL,NULL,'0','0','system:test','#',NULL,NULL,NULL,NULL,0,NULL);
INSERT INTO test.sys_role (name,role_key,status,del_flag,create_by,create_time,update_by,update_time,remark) VALUES
('admin','admin','0',0,NULL,NULL,NULL,NULL,NULL),
('test','test','0',0,NULL,NULL,NULL,NULL,NULL);
INSERT INTO test.sys_user (user_name,nick_name,password,status,email,phone_number,sex,avatar,user_type,create_by,create_time,update_by,update_time,del_flag) VALUES
('tom','tom','$2a$10$M5W1uFxQW7jvWNEe9.Cs3.ENIQjy8P/.un3v8plySy.XdrY/mqRua','0',NULL,NULL,NULL,NULL,'1',NULL,NULL,NULL,NULL,0);
INSERT INTO test.sys_user_role (role_id) VALUES(1);
INSERT INTO test.sys_role_menu (menu_id) VALUES(1),(2);
5.6 从数据库查询权限信息
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 = 1 # user_id有变化
and r.status = '0'
and m.status = '0'
5.7 代码实现
我们只需要根据用户 id
去查询到其所对应的权限信息即可。
所以我们可以先定义个 mapper
,其中提供一个方法可以根据 userid
查询权限信息。
package com.example.springsecuritydemo2.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
/**
* @author test
*/
@TableName(value = "sys_menu")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
@TableId
private Long id;
private String menuName;
private String path;
private String component;
private String visible;
private String status;
private String perms;
private String icon;
private String createBy;
private Date createTime;
private String updateBy;
private Date updateTime;
private String delFlag;
private String remark;
}
package com.example.springsecuritydemo2.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springsecuritydemo2.entity.Menu;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @author test
*/
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
/**
* 根据用户id查询权限信息
*
* @param userId
* @return
*/
List<String> selectMenuByUserId(Long userId);
}
尤其是自定义方法,所以需要创建对应的 mapper
文件,定义对应的 sql
语句。
<?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.springsecuritydemo2.mapper.MenuMapper">
<select id="selectMenuByUserId" resultType="java.lang.String">
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>
mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
package com.example.springsecuritydemo2;
import com.example.springsecuritydemo2.mapper.MenuMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class SpringSecurityDemo2ApplicationTests {
@Autowired
private MenuMapper menuMapper;
@Test
void testMenuMapper() {
List<String> list = menuMapper.selectMenuByUserId(1L);
System.out.println(list);
}
}
修改 LoginUser
整理用户权限:
package com.example.springsecuritydemo2.entity;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
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.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* @author test
*/
@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class LoginUser implements UserDetails, Serializable {
private static final long serialVersionUID = 1L;
private User user;
private List<String> permissions;
private List<SimpleGrantedAuthority> authorities;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (!Objects.isNull(authorities)) {
return authorities;
}
// 把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
authorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
return authorities;
}
@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;
}
}
修改 UserDetailsServiceImpl
获取用户权限:
package com.example.springsecuritydemo2.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.springsecuritydemo2.entity.LoginUser;
import com.example.springsecuritydemo2.entity.User;
import com.example.springsecuritydemo2.mapper.MenuMapper;
import com.example.springsecuritydemo2.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
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.List;
import java.util.Objects;
/**
* @author test
*/
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private UserMapper userMapper;
@Autowired
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
private MenuMapper menuMapper;
@Autowired
public void setMenuMapper(MenuMapper menuMapper) {
this.menuMapper = menuMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("loadUserByUsername");
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(lambdaQueryWrapper);
// 没有查询到用户抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误!");
}
// 查询权限信息
List<String> list = menuMapper.selectMenuByUserId(user.getId());
// 把用户封装成UserDetails
return new LoginUser(user, list);
}
}
对 SimpleGrantedAuthority
序列化:
package com.example.springsecuritydemo2.entity;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.ObjectUtils;
import java.io.IOException;
import java.util.Iterator;
/**
* @author test
*/
public class SimpleGrantedAuthorityDeserializer extends StdDeserializer<SimpleGrantedAuthority> {
public SimpleGrantedAuthorityDeserializer() {
super(SimpleGrantedAuthority.class);
}
@Override
public SimpleGrantedAuthority deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode jsonNode = p.getCodec().readTree(p);
Iterator<JsonNode> elements = jsonNode.elements();
while (elements.hasNext()) {
JsonNode next = elements.next();
JsonNode authority = next.get("authority");
if (ObjectUtils.isEmpty(authority)) {
continue;
}
return new SimpleGrantedAuthority(authority.asText());
}
return null;
}
}
修改 RedisConfig
:
om.registerModule(new SimpleModule().addDeserializer(SimpleGrantedAuthority.class, new SimpleGrantedAuthorityDeserializer()));
修改 JwtAuthenticationTokenFilter
存入用户权限:
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
6、自定义失败处理
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的 json
,这样可以让前端能
对响应进行统一的处理,要实现这个功能我们需要知道 SpringSecurity
的异常处理机制。
在 SpringSecurity
中,如果我们在认证或者授权的过程中出现了异常会被 ExceptionTranslationFilter
捕
获到,在 ExceptionTranslationFilter
中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成 AuthenticationException
,然后调用 AuthenticationEntryPoint
对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成 AccessDeniedException
,然后调用 AccessDeniedHandler
对象的
方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义 AuthenticationEntryPoint
和
AccessDeniedHandler
,然后配置给 SpringSecurity
即可。
6.1 自定义实现类
package com.example.springsecuritydemo2.handler;
import com.alibaba.fastjson.JSON;
import com.example.springsecuritydemo2.entity.ResponseResult;
import com.example.springsecuritydemo2.util.WebUtil;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author test
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
/**
* 授权失败
*
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 处理异常
ResponseResult<String> responseResult = new ResponseResult<>(HttpStatus.FORBIDDEN.value(), "没有权限,请授予用户权限", null);
String json = JSON.toJSONString(responseResult);
WebUtil.renderString(httpServletResponse, json);
}
}
package com.example.springsecuritydemo2.handler;
import com.alibaba.fastjson.JSON;
import com.example.springsecuritydemo2.entity.ResponseResult;
import com.example.springsecuritydemo2.util.WebUtil;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author test
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
/**
* 认证失败的处理逻辑
*
* @param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 处理异常
ResponseResult<String> responseResult = new ResponseResult<>(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请检查用户名和密码", null);
String json = JSON.toJSONString(responseResult);
WebUtil.renderString(httpServletResponse, json);
}
}
5.2 配置给SpringSecurity
我们可以使用 HttpSecurity
对象的方法去配置。
private AccessDeniedHandler accessDeniedHandler;
@Autowired
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
this.accessDeniedHandler = accessDeniedHandler;
}
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
this.authenticationEntryPoint = authenticationEntryPoint;
}
// 配置异常处理器
http.exceptionHandling()
// 配置认证失败处理器
.authenticationEntryPoint(authenticationEntryPoint)
// 配置授权失败处理器
.accessDeniedHandler(accessDeniedHandler);
7、跨域
浏览器出于安全的考虑,使用 XMLHttpRequest
对象发起 HTTP
请求时必须遵守同源策略,否则就是跨域的
HTTP
请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一
致。
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。
所以我们就要处理一下,让前端能进行跨域请求。
7.1 对SpringBoot配置允许跨域请求
package com.example.springsecuritydemo2.config;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author test
*/
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "PUT", "DELETE")
// 设置允许的header属性
.allowedHeaders()
// 跨域允许时间
.maxAge(3600);
}
}
7.2 开启SpringSecurity的跨域访问
由于我们的资源都会收到 SpringSecurity
的保护,所以想要跨域访问还要让 SpringSecurity
运行跨域访问。
// 允许跨域
http.cors();
8、其它权限校验方法
我们前面都是使用 @PreAuthorize
注解,然后在在其中使用的是 hasAuthority
方法进行校验。
SpringSecurity
还为我们提供了其它方法,例如:hasAnyAuthority
,hasRole
,hasAnyRole
等。
并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。
hasAuthority
方法实际是执行到了 SecurityExpressionRoothasAuthority
,大家只要断点调试即可知道它
内部的校验原理。它内部其实是调用 authentication
的 getAuthorities
方法获取用户的权限列表,然后判断
我们存入的方法参数数据在权限列表中。
hasAnyAuthority
方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
@PreAuthorize("hasAnyAuthority('system:test','system:admin')")
hasRole
要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_
后再去比较,所以这种
情况下要用用户对应的权限也要有 ROLE_
这个前缀才可以。
@PreAuthorize("hasRole('system:test')")
hasAnyRole
有任意的角色就可以访问,它内部也会把我们传入的参数拼接上 ROLE_
后再去比较,所以这种情况
下要用用户对应的权限也要有 ROLE_
这个前缀才可以。
@PreAuthorize("hasAnyRole('system:test','system:admin')")
9、自定义权限校验方法
我们也可以定义自己的权限校验方法,在 @PreAuthorize
注解中使用我们的方法。
package com.example.springsecuritydemo2.expression;
import com.example.springsecuritydemo2.entity.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author test
*/
@Component(value = "ex")
public class ExpressionRoot {
public boolean hasAuthority(String authority) {
// 获取当前用户权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> list = loginUser.getPermissions();
// 判断用户权限集合是否存在authority
return list.contains(authority);
}
}
在 SPEL
表达式中使用 @ex
相当于获取容器中 bean
的名字为 ex
的对象,然后再调用这个对象的
hasAuthority
方法。
@PreAuthorize("@ex.hasAuthority('system:test')")
10、基于配置的权限控制
我们也可以在配置类中使用使用配置的方式对资源进行权限控制。
// 手动配置权限控制
.antMatchers("/hello/world").hasAuthority("system:test")
11、CSRF
CSRF
是指跨站请求伪造(Cross-site request forgery),是 web
常见的攻击之一。
SpringSecurity
去防止 CSRF
攻击的方式就是通过 csrf_token
,后端会生成一个 csrf_token
,前端发起请
求的时候需要携带这个 csrf_token
,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现 CSRF
攻击依靠的是 cookie
中所携带的认证信息,但是在前后端分离的项目中我们的认证信息其
实是 token
,而 token
并不是存储中 cookie
中,并且需要前端代码去把 token
设置到请求头中才可以,所以
CSRF
攻击也就不用担心了。
12、认证成功处理器
实际上在 UsernamePasswordAuthenticationFilter
进行登录认证的时候,如果登录成功了是会调用
AuthenticationSuccessHandler
的方法进行认证成功后的处理的,AuthenticationSuccessHandler
就是登
录成功处理器,我们也可以自己去自定义成功处理器进行成功后的相应处理。使用前面的demo。
package com.example.springsecuritydemo1.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author test
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("认证成功了!");
}
}
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
http.formLogin().successHandler(authenticationSuccessHandler);
13、认证失败处理器
实际上在 UsernamePasswordAuthenticationFilter
进行登录认证的时候,如果认证失败了是会调用
AuthenticationFailureHandler
的方法进行认证失败后的处理的。
AuthenticationFailureHandler
就是登录失败处理器。
我们也可以自己去自定义失败处理器进行失败后的相应处理。
package com.example.springsecuritydemo1.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author test
*/
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("认证失败了!");
}
}
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
http.formLogin().successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler);
14、认证退出成功处理器
package com.example.springsecuritydemo1.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author test
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("注销成功!");
}
}
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
public void setLogoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) {
this.logoutSuccessHandler = logoutSuccessHandler;
}
http.logout().logoutSuccessHandler(logoutSuccessHandler);