SpringSecurity前后端分离(动态鉴权)
一、认证流程讲解
1、原始认证流程
原始认证流程通常会配合Session一起使用,但前后端分离后就用不到Session了
SpringSecurity默认的认证流程如下图(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)
DaoAuthenticationProvider
继承AbstractUserDetailsAuthenticationProvider
抽象类,而AbstractUserDetailsAuthenticationProvider
抽象类又实现了AuthenticationProvider
这个接口。
AuthenticationProvider
接口和AuthenticationManager
接口都有authenticate()
这个方法认证流程:
1、传入用户名和密码
2、
UsernamePasswordAuthenticationFilter
会把用户名和密码封装成Authentication对象3、然后又再调用
AuthenticationManager
接口中的authenticate()
方法进行认证,在AuthenticationManager
接口的实现类ProviderManager
中又调用了重写的authenticate()
方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider
中重写了authenticate()
方法4、
AbstractUserDetailsAuthenticationProvider
的authenticate()
方法中调用了抽象方法retrieveUser()
方法5、
DaoAuthenticationProvider
在重写方法retrieveUser()
里调用了loadUserByUsername()
方法6、
loadUserByUsername()
方法会返回UserDetails
对象,认证成功逐一返回上一层
2、前后端分离认证流程
前后端分离后,我们要求在认证成功或者失败的时候能够返回对应的状态码,这时我们不再使用Session进行认证管理,而常采用jwt(JSON Web Token)的方式进行认证,这里引出两种前后端分离的写法
(该图是B站UP主“三更草堂”讲SpringSecurity课程的图)
无论使用下面哪一种写法,这里都需要在
UsernamePasswordAuthenticationFilter
前面添加一个过滤器,用于进行Token认证,如果Token认证成功,则表示该用户已登录;Token认证失败则表明未登录或者登陆已过期。
2.1、继承UsernamePasswordAuthenticationFilter
的写法
认证流程:
1、传入用户名和密码
2、
MyUsernamePasswordAuthenticationFilter
会把用户名和密码封装成Authentication对象3、然后又再调用
AuthenticationManager
接口中的authenticate()
方法进行认证,在AuthenticationManager
接口的实现类ProviderManager
中又调用了重写的authenticate()
方法进行认证。抽象类AbstractUserDetailsAuthenticationProvider
中重写了authenticate()
方法4、
AbstractUserDetailsAuthenticationProvider
的authenticate()
方法中调用了抽象方法retrieveUser()
方法5、
DaoAuthenticationProvider
在重写方法retrieveUser()
里调用了loadUserByUsername()
方法,自定义AuthUserDetailsServiceImpl
类实现UserDetailsService
接口,重写loadUserByUsername()
方法6、在
loadUserByUsername()
方法中,会查询用户和角色,然后返回UserDetails
对象7、在继承
WebSecurityConfigurerAdapter
的类中设置登陆成功、失败处理器,处理器内部定义好返回的状态码等信息
2.2、自定义写法
UsernamePasswordAuthenticationToken
继承了AbstractAuthenticationToken
抽象类,AbstractAuthenticationToken
抽象类实现了Authentication
接口认证流程:
1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层
2、Service层创建
UsernamePasswordAuthenticationToken
对象,把用户名和密码封装成Authentication
对象3、然后调用
AuthenticationManager
的authenticate()
方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider
中重写了authenticate()
方法4、
AbstractUserDetailsAuthenticationProvider
的authenticate()
方法中调用了抽象方法retrieveUser()
方法5、
DaoAuthenticationProvider
在重写方法retrieveUser()
里调用了loadUserByUsername()
方法,自定义AuthUserDetailsServiceImpl
类实现UserDetailsService
接口,重写loadUserByUsername()
方法6、在
loadUserByUsername()
方法中,会查询用户和角色,然后返回UserDetails
对象
2.3、区别
1、使用
UsernamePasswordAuthenticationFilter
的写法需要使用登陆成功、失败处理器,自定义的写法不需要,自定义的写法可以自定义失败处理器(包括认证异常和授权异常,即登陆失败和没有权限)2、使用
UsernamePasswordAuthenticationFilter
的写法对于扩展写法没那么友好,比如说添加手机验证码
二、数据库的设计
该示例是上面自定义的前后端分离的写法
这里使用的是Oracle数据库,这里没有权限的表,但是使用角色来判断也差不多
1、用户表
2、用户角色关系表
3、角色表
4、图片表
5、点赞表
三、初始配置
SpringBoot 版本是 2.6.0
1、项目结构
2、导入依赖
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- SpringSecurity -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--MyBatis-Plus的依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</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.74</version>
</dependency>
<!--hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.6</version>
</dependency>
<!-- mybatis-plus-generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<!--添加 模板引擎 依赖,MyBatis-Plus 支持 Velocity(默认)-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<!--swagger的依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<!-- JWT的依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Oracle数据库 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
3、代码生成器
代码生成器这里最开始使用的是mysql 8.X版本的,读者需要自己修改一下数据库的名字,如果是mysql 5.X还需要修改一下驱动
后面才改用Oracle数据库,这里的代码就懒得改了
package com.guet.APPshareimage;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.apache.commons.lang3.StringUtils;
import java.util.Scanner;
/**
* @Author LZDWTL
* @Date 2021-12-15 17:09
* @ClassName CodeGenerator
* @Description 代码生成器
*/
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 创建代码生成器对象
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(scanner("请输入你的项目路径") + "/src/main/java");
//作者
gc.setAuthor("LZDWTL");
//生成之后是否打开资源管理器
gc.setOpen(false);
//重新生成时是否覆盖文件
gc.setFileOverride(false);
//%s 为占位符
//mp生成service层代码,默认接口名称第一个字母是有I
gc.setServiceName("%sService");
//设置主键生成策略 自动增长
gc.setIdType(IdType.AUTO);
//设置Date的类型 只使用 java.util.date 代替
gc.setDateType(DateType.ONLY_DATE);
//开启实体属性 Swagger2 注解
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/shareimage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("shareimage");
dsc.setPassword("888888");
//使用mysql数据库
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
//pc.setModuleName(scanner("请输入模块名"));
pc.setParent("com.guet.APPshareimage");
pc.setController("controller");
pc.setService("service");
pc.setServiceImpl("service.impl");
pc.setMapper("mapper");
pc.setEntity("entity");
pc.setXml("mapper");
mpg.setPackageInfo(pc);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
//设置哪些表需要自动生成
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
//实体类名称驼峰命名
strategy.setNaming(NamingStrategy.underline_to_camel);
//列名名称驼峰命名
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//使用简化getter和setter
strategy.setEntityLombokModel(true);
//设置controller的api风格 使用RestController
strategy.setRestControllerStyle(true);
//驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
//忽略表中生成实体类的前缀
//strategy.setTablePrefix("t_");
mpg.setStrategy(strategy);
mpg.execute();
}
}
运行代码生成器,复制路径输入,然后依次输入数据库中表的名字
D:\WorkSpace\JavaWorkSpce\ideal\APP-shareimage\APP-shareimage
t_user,t_picture,t_like,t_user_role,t_role
4、配置application.yml
根据自己的数据库和redis进行配置
server:
port: 8080
spring:
# 数据库配置
datasource:
driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@120.77.80.135:1521:orcl
username: XXXXXX
password: XXXXXX
# 连接池
hikari:
# 连接池名
pool-name: DateHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接最大存活时间,默认600000(10分钟)
idle-timeout: 180000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接自动提交
auto-commit: true
# 连接最大存活时间,1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
#connection-test-query: SELECT 1 #这个是mysql的测试语句
connection-test-query: SELECT * from dual #这个是oracle的测试语句
#redis配置
redis:
#服务器地址
host: 120.77.80.135
#端口
port: 6379
#redis密码
password: XXXXXX
#数据库,默认是0
database: 0
#超时时间
timeout: 1209600000ms
lettuce:
pool:
#最大链接数,默认8
max-active: 8
#最大连接阻塞等待时间,默认-1
max-wait: 10000ms
#最大空闲连接,默认8
max-idle: 200
#最小空闲连接,默认0
min-idle: 5
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.guet.APPshareimage.entity
logging:
level:
com.guet.shareimage.mapper: debug
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: lzdwtl
# JWT的超期限时间(1000*60*60*24*14)14天,即两周
expiration: 1209600000
# JWT 负载中拿到开头
tokenHead: Bearer
role:
roleid: 1
5、其他配置、工具类
5.1、SpringSecurity配置类
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
}
}
5.2、JSON格式返回配置类
public abstract class JSONAuthentication {
/**
* 输出JSON
*
* @param request
* @param response
* @param obj
* @throws IOException
* @throws ServletException
*/
protected void WriteJSON(HttpServletRequest request,
HttpServletResponse response,
Object obj) throws IOException, ServletException {
//这里很重要,否则页面获取不到正常的JSON数据集
response.setContentType("application/json;charset=UTF-8");
//跨域设置
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Method", "POST,GET");
//输出JSON
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(obj));
out.flush();
out.close();
}
}
5.3、密码编码类
@Component
public class BCryptPasswordEncoderUtil extends BCryptPasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return super.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return super.matches(rawPassword,encodedPassword);
}
}
5.4、JWT工具类
@Component
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
private static String SECRET_KEY;
private static Long EXPIRATION_TIME;
//对于静态变量,需要使用set方法才能使用设置好的字段值
@Value("${jwt.secret}")
public void setSECRET_KEY(String SECRET_KEY) {
this.SECRET_KEY = SECRET_KEY;
}
@Value("${jwt.expiration}")
public void setEXPIRATION_TIME(Long expiration) {
this.EXPIRATION_TIME = expiration;
}
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 = EXPIRATION_TIME;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("LZDWTL") // 签发者
.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();
}
/**
* 生成加密后的秘钥 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(SECRET_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();
}
}
5.5、Redis工具类
@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);
}
}
5.6、Redis配置类
package com.guet.config;
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;
/**
* @Author LZDWTL
* @Date 2022-01-30 19:39
* @ClassName
* @Description
*/
@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;
}
}
5.7、序列化工具
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);
}
}
四、全局异常处理
1、公用返回对象
1.1、拓展接口
使公用返回对象枚举类和自定义异常方便扩展
/**
* @Author LZDWTL
* @Date 2021-12-06 15:59
* @ClassName CommonResp
* @Description 返回对象的接口,装饰者模式
*/
public interface CommonResp {
Integer getCode();
String getMsg();
CommonResp setMsg(String msg);
}
1.2、公用返回对象
@Data
public class RespBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer code;
private String msg;
private Object obj;
public RespBean(RespBeanEnum respBeanEnum, Object obj) {
this.code = respBeanEnum.getCode();
this.msg = respBeanEnum.getMsg();
this.obj = obj;
}
public RespBean(RespBeanEnum respBeanEnum) {
this.code = respBeanEnum.getCode();
this.msg = respBeanEnum.getMsg();
}
public RespBean(RespBeanEnum respBeanEnum, String msg) {
this.code = respBeanEnum.getCode();
this.msg = msg;
}
public RespBean() {
this.code = RespBeanEnum.ERROR.getCode();
this.msg = RespBeanEnum.ERROR.getMsg();
}
public RespBean(String msg) {
this.code = RespBeanEnum.ERROR.getCode();
this.msg = msg;
}
//自定义的业务异常错误码和信息
public RespBean(ServicesException e) {
this.code = e.getCode();
this.msg = e.getMsg();
}
}
1.3、枚举类
public enum RespBeanEnum implements CommonResp{
SUCCESS(200,"请求成功!"),
ERROR(500,"服务器响应错误!"),
/** 10XX 表示用户错误*/
USER_REGISTER_FAILED(1001, "注册失败"),
USER_ACCOUNT_EXISTED(1002,"用户名已存在"),
USER_ACCOUNT_NOT_EXIST(1003,"用户名不存在"),
USERNAME_PASSWORD_ERROR(1004,"用户名或密码错误"),
PASSWORD_ERROR(1005,"密码错误"),
USER_ACCOUNT_EXPIRED(1006,"账号过期"),
USER_PASSWORD_EXPIRED(1007,"密码过期"),
USER_ACCOUNT_DISABLE(1008,"账号不可用"),
USER_ACCOUNT_LOCKED(1009,"账号锁定"),
USER_NOT_LOGIN(1010,"用户未登陆"),
USER_NO_PERMISSIONS(1011,"用户权限不足"),
USER_SESSION_INVALID(1012,"会话已超时"),
USER_ACCOUNT_LOGIN_IN_OTHER_PLACE(1013,"账号超时或账号在另一个地方登陆"),
TOKEN_VALIDATE_FAILED(1014,"Token令牌验证失败"),
LIKE_ALREADY_GICED(1015,"请勿重复点赞"),
/** 20XX 表示服务器错误 */
PICTURE_UPLOAD_FAILED(2001,"上传图片失败"),
GIVE_LIKE_FAILED(2002,"点赞失败"),
PICTURE_LOAD_FAILED(2003,"图片加载失败"),
UPDATE_USER_INFO_FAILED(2004,"修改用户信息失败"),
UPDATE_USER_PASSWORD_FAILED(2005,"修改密码失败"),
;
private Integer code;
private String msg;
RespBeanEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getMsg() {
return this.msg;
}
@Override
public CommonResp setMsg(String msg) {
this.msg=msg;
return this;
}
}
2、全局异常
2.1、自定义异常
实现
CommonResp
接口,方便自定义异常后续修改错误信息
public class ServicesException extends RuntimeException implements CommonResp {
private CommonResp commonResp;
//直接接收RespBeanEnum的传参用于构造业务异常
public ServicesException(CommonResp commonResp) {
super(); //调用父类的无参构造方法
this.commonResp = commonResp;
}
//接收自定义msg的方式构造业务异常
public ServicesException(String msg, CommonResp commonResp) {
super();
this.commonResp = commonResp;
this.commonResp.setMsg(msg);
}
@Override
public Integer getCode() {
return this.commonResp.getCode();
}
@Override
public String getMsg() {
return this.commonResp.getMsg();
}
@Override
public CommonResp setMsg(String msg) {
this.commonResp.setMsg(msg);
return this;
}
}
2.2、全局异常处理器
@RestControllerAdvice
注解表示捕获控制层抛出的异常
@ExceptionHandler
注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常
(图片来源:https://blog.csdn.net/weixin_43702146/article/details/118606502)
因为使用了
@RestControllerAdvice
注解,自动去捕获控制层抛出的异常,AuthenticationException
异常和AccessDeniedException
异常也被捕获了,但是我不想在这里处理,所以将这两个异常往外抛给失败处理器去处理。
@RestControllerAdvice //捕获controller层的异常
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* @Author: LZDWTL
* @param: [e]
* @return: com.guet.shareimage.response.RespBean
* @Description: 业务异常
*/
@ExceptionHandler(value = ServicesException.class)
public RespBean servicesExceptionHandler(ServicesException e){
logger.error("发生业务异常! 原因是:{}",e.getMsg());
return new RespBean(e);
}
/**
* @Author: LZDWTL
* @param: [e]
* @return: com.guet.shareimage.response.RespBean
* @Description: 其他异常
*/
@ExceptionHandler(value = Exception.class)
public RespBean exceptionHandler(Exception e){
logger.error("未知异常! 原因是:",e);
return new RespBean();
}
/**
* @Author: LZDWTL
* @Date: 2022/2/11
* @param: [authException]
* @return: void
* @Description: 将 AuthenticationException 异常往上抛,让认证处理器去处理
*/
@ExceptionHandler(value = AuthenticationException.class)
public void accountExpiredExceptionHandler(AuthenticationException authException){
throw authException;
}
//将 AccessDeniedException 异常往上抛,让授权处理器去处理
@ExceptionHandler(value = AccessDeniedException.class)
public void accessDeniedExceptionHandler(AccessDeniedException accDenException){
throw accDenException;
}
}
五、登陆认证
UsernamePasswordAuthenticationToken
继承了AbstractAuthenticationToken
抽象类,AbstractAuthenticationToken
抽象类实现了Authentication
接口认证流程:
1、前端通过把用户名和密码发送到后端的控制器,控制器调用业务层
2、Service层创建
UsernamePasswordAuthenticationToken
对象,把用户名和密码封装成Authentication
对象3、然后调用
AuthenticationManager
的authenticate()
方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider
中重写了authenticate()
方法4、
AbstractUserDetailsAuthenticationProvider
的authenticate()
方法中调用了抽象方法retrieveUser()
方法5、
DaoAuthenticationProvider
在重写方法retrieveUser()
里调用了loadUserByUsername()
方法,自定义AuthUserDetailsServiceImpl
类实现UserDetailsService
接口,重写loadUserByUsername()
方法6、在
loadUserByUsername()
方法中,会查询用户和角色,然后返回UserDetails
对象
1、登陆模块
包括登陆和登出功能
1.1、控制器LoginController
/**
* @Author LZDWTL
* @Date 2021-12-17 8:57
* @ClassName LoginController
* @Description 登陆控制器
* 这个控制器没有用到,“/login”这个url是SpringSecurity中的UsernamePasswordAuthenticationFilter拦截器中自己设定的
* 同时它还设置了必须使用POST方式才能进行登陆
*/
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/doLogin")
public RespBean doLogin(@RequestBody LoginDTO loginDTO){
return loginService.doLogin(loginDTO);
}
@RequestMapping("/doLogout")
public RespBean doLogout(){
return loginService.doLogout();
}
}
1.2、业务层
Service层创建
UsernamePasswordAuthenticationToken
对象,把用户名和密码封装成Authentication
对象
1.2.1、LoginService
public interface LoginService {
RespBean doLogin(LoginDTO loginDTO);
RespBean doLogout();
}
1.2.2、LoginServiceImpl
这里的
AuthenticationManager
需要在SpringSecurity
中使用authenticationManagerBean()
方法才能调用AuthenticationManager
的authenticate()
方法进行认证,抽象类AbstractUserDetailsAuthenticationProvider
中重写了authenticate()
方法这里把生成的Token和查询到的用户信息存到Redis中,方便后续使用
@Service
public class LoginServiceImpl implements LoginService {
private static final Logger logger = LoggerFactory.getLogger(LoginServiceImpl.class);
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private TUserService userService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoderUtil passwordEncoder;
@Autowired
private RedisCache redisCache;
/**
* @Author: LZDWTL
* @param: [username, password]
* @return: com.guet.APPshareimage.response.RespBean
* @Description: 登陆
*/
@Override
public RespBean doLogin(LoginDTO loginDTO) {
/**
* 因为我使用了全局异常处理,GobalExceptionHandler会自动捕获controller层抛出的异常
* authenticationManager.authenticate 这一句认证失败会抛出AuthenticationException异常
* 我定义了认证失败处理器无法获取到 AuthenticationException 异常,因为全局异常处理已经捕获了
* 然后 AuthenticationException 异常不属于 ServicesException,所以会返回500,服务器响应错误
*/
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
//用户名密码错误
throw new ServicesException(RespBeanEnum.USERNAME_PASSWORD_ERROR);
}
AuthUser authUser = (AuthUser) authenticate.getPrincipal();
String username = authUser.getTUser().getUsername();
String token = JwtUtil.createJWT(username);
//把token和用户信息存到redis中
redisCache.setCacheObject("Token_" + username, token);
redisCache.setCacheObject("UserDetails_" + username, authUser);
//将用户存入上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
Map<String, String> map = new HashMap<>();
map.put("token", token);
return new RespBean(RespBeanEnum.SUCCESS, map);
}
@Override
public RespBean doLogout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
AuthUser authUser = (AuthUser) authentication.getPrincipal();
String username = authUser.getTUser().getUsername();
//删除redis中存的信息
redisCache.deleteObject("Token_" + username);
redisCache.deleteObject("UserDetails_" + username);
//清除上下文
SecurityContextHolder.clearContext();
return new RespBean(RespBeanEnum.SUCCESS);
}
}
1.2.3、TUserService
public interface TUserService extends IService<TUser> {
TUser getUserByUserName(String username);
}
1.2.4、TUserServiceImpl
TUserMapper
需要继承BaseMapper
才能使用selectOne()
这个方法
@Service
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements TUserService {
@Value("${role.roleid}")
private Integer roleId;
@Autowired
private TUserMapper userMapper;
/**
* @Author: LZDWTL
* @Date: 2021/12/28
* @param: [username]
* @return: com.guet.response.RespBean
* @Description: 通过用户名获取用户
*/
@Override
public TUser getUserByUserName(String username) {
LambdaQueryWrapper<TUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//查询条件:全匹配账号名,和状态为1的账号
lambdaQueryWrapper
.eq(TUser::getUsername, username);
//用getOne查询一个对象出来
// TUser user = this.getOne(lambdaQueryWrapper);
TUser user = userMapper.selectOne(lambdaQueryWrapper); //这个与上面的getOne有无区别?
return user;
}
}
1.3、实现 UserDetails
接口
/**
* @Author LZDWTL
* @Date 2021-12-15 23:35
* @ClassName AuthUser
* @Description 实现UserDetails,仿写User的原因是 防止User类名和自己创建的实体类 User 重合(虽然我这里创建的不是User而是TUser)
*/
@Data
@AllArgsConstructor //全参构造
@NoArgsConstructor //无参构造
public class AuthUser implements UserDetails {
private TUser tUser;
// @JSONField(serialize = false)
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return tUser.getPassword();
}
@Override
public String getUsername() {
return tUser.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;
}
}
1.4、实现UserDetailsService
接口
重写
UserDetailsService
接口的loadUserByUsername()
方法,在loadUserByUsername()
方法中,会查询用户和权限(这里没有权限表,所以查的是角色),然后返回UserDetails
对象
/**
* 要实现UserDetailsService接口,这个接口是security提供的
*/
@Service(value = "userDetailsService")
public class AuthUserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(AuthUserDetailsServiceImpl.class);
@Autowired
private TUserService userService;
@Autowired
private TRoleService roleService;
/**
* 通过账号查找用户、角色的信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TUser user = userService.getUserByUserName(username);
if (user == null) {
//用户名不存在
throw new ServicesException(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);
} else {
//查找角色,实际应该查询权限,但我数据库没有设计所以就查角色就好了
List<String> roles = roleService.getRolesByUserName(username);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
System.out.println("AuthUserDetailsServiceImpl-loadUserByUsername......user ===> " + user);
return new AuthUser(user, authorities);
}
}
}
1.5、Mapper
1.5.1、TUserMapper
@Mapper
public interface TUserMapper extends BaseMapper<TUser> {
}
2、Token 认证模块
2.1、认证过滤器
/**
* @Author LZDWTL
* @Date 2021-12-20 16:28
* @ClassName ${MyOncePerRequestFilter}
* @Description ${认证过滤器}
*/
@Component
public class MyOncePerRequestFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(MyOncePerRequestFilter.class);
@Value("${jwt.tokenHeader}")
private String header;
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// header的值是在yml文件中定义的 “Authorization”
String token = request.getHeader(header);
System.out.println("MyOncePerRequestFilter-token = " + token);
if (!StrUtil.isEmpty(token)) {
String username = null;
try {
Claims claims = JwtUtil.parseJWT(token);
username = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
// throw new ServicesException("非法Token,请重新登陆", RespBeanEnum.ERROR);
WriteJSON(request,response,new RespBean(RespBeanEnum.ERROR,"非法Token,请重新登陆"));
return;
}
String redisToken = redisCache.getCacheObject("Token_" + username);
System.out.println("MyOncePerRequestFilter-redisToken = " + redisToken);
if (StrUtil.isEmpty(redisToken)) {
//token令牌验证失败
// throw new ServicesException(RespBeanEnum.TOKEN_VALIDATE_FAILED);
//输出JSON
WriteJSON(request,response,new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED));
return;
}
//对比前端发送请求携带的的token是否与redis中存储的一致
if (!Objects.isNull(redisToken) && redisToken.equals(token)) {
AuthUser authUser = redisCache.getCacheObject("UserDetails_" + username);
System.out.println("MyOncePerRequestFilter-authUser = " + authUser);
if (Objects.isNull(authUser)) {
// throw new ServicesException(RespBeanEnum.USER_NOT_LOGIN);
WriteJSON(request,response,new RespBean(RespBeanEnum.USER_NOT_LOGIN));
return;
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
chain.doFilter(request, response);
}
private void WriteJSON(HttpServletRequest request,
HttpServletResponse response,
Object obj) throws IOException, ServletException {
//这里很重要,否则页面获取不到正常的JSON数据集
response.setContentType("application/json;charset=UTF-8");
//跨域设置
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Method", "POST,GET");
//输出JSON
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(obj));
out.flush();
out.close();
}
}
2.2、SpringSecuity配置类
在配置类中使用
addFilterBefore()
方法让认证过滤器MyOncePerRequestFilter
添加在UsernamePasswordAuthenticationFilter
这个过滤器前面
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
//3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
六、鉴权
下面列举了两种鉴权方式,分别是注解鉴权和动态鉴权
1、注解鉴权
使用
@PreAuthorize
注解需要在SpringSecurity
配置类中使用下面的语句才能开启方法级的安全
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RestController
@RequestMapping("/user")
public class TUserController {
@RequestMapping("/hello")
//对于hasRole这个方法来讲,ROLE_ 加不加都可以,它的方法会自动判断的
@PreAuthorize("hasRole('ROLE_user')")
public String test() {
return "Hello Login Success!";
}
}
这样就可以了,因为前面已经写好了一些关联的代码,所以在访问该URL的时候,会执行hasRole()
这个方法,然后查询AuthUser
类(AuthUser
类就是实现了UserDetails
接口的实现类)中的属性authorities
,只要authorities
中包含"ROLE_user
",则该用户就可以访问这个URL,否则会报错,提示权限不足。
注意访问一些需要认证后才能访问的URL时,记得带上token和content-type。
我这里的token的key是Authorization,这个是在
application.yml
文件中定义的,可以自行修改
2、动态鉴权
这里写的动态鉴权需要数据库中新创建两个表,分别是菜单表
t_menu
和角色菜单关系表t_role_menu
,菜单表中存放前端需要访问的url地址
下面编写鉴权类
@Component("rbacService")
public class MyRBACService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
UserDetails userDetails=(UserDetails)principal;
/**
* 该方法主要对比认证过的用户是否具有请求URL的权限,有则返回true
*/
//本次要访问的资源
SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(request.getRequestURI());
//用户拥有的权限中是否包含请求的url
return userDetails.getAuthorities().contains(simpleGrantedAuthority);
}
return false;
}
}
在SpringSecurity配置类中设置鉴权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/user/doLogin").permitAll()
// .antMatchers("/swagger-ui.html","/user/test").permitAll()
//其余url都要认证才能访问
// .anyRequest().authenticated()
//鉴权规则
.anyRequest().access("@rbacService.hasPermission(request,authentication)");
//3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
//4、异常处理
http
.exceptionHandling()
//认证失败处理器
.authenticationEntryPoint(myAuthenticationEntryPoint)
//权限不足处理器
.accessDeniedHandler(myAccessDeniedHandler);
//5、允许跨域
http.cors();
}
七、自定义失败处理器
1、认证失败处理器
继承自定义的JSON格式输出类
JSONAuthentication
输出JSON格式,同时在里面判断是什么异常做针对性输出
@Component
public class MyAuthenticationEntryPoint extends JSONAuthentication implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(MyAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//用户未登录或者身份校验失败
// RespBean respBean = new RespBean(RespBeanEnum.TOKEN_VALIDATE_FAILED);
// this.WriteJSON(request, response, respBean);
RespBean respBean;
if (authException instanceof AccountExpiredException) {
//账号过期
respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_EXPIRED);
} else if (authException instanceof InternalAuthenticationServiceException) {
//用户不存在
respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);
} else if (authException instanceof BadCredentialsException) {
//用户名或密码错误(也就是用户名匹配不上密码)
respBean = new RespBean(RespBeanEnum.USERNAME_PASSWORD_ERROR);
} else if (authException instanceof CredentialsExpiredException) {
//密码过期
respBean = new RespBean(RespBeanEnum.USER_PASSWORD_EXPIRED);
} else if (authException instanceof DisabledException) {
//账号不可用
respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_DISABLE);
} else if (authException instanceof LockedException) {
//账号锁定
respBean = new RespBean(RespBeanEnum.USER_ACCOUNT_LOCKED);
} else {
//其他错误
respBean = new RespBean(RespBeanEnum.USER_NOT_LOGIN);
}
//打印错误
logger.error(String.valueOf(authException));
//输出
this.WriteJSON(request, response, respBean);
}
}
2、权限不足处理器
@Component
public class MyAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//用户权限不足
RespBean respBean = new RespBean(RespBeanEnum.USER_NO_PERMISSIONS);
//输出
this.WriteJSON(request, response, respBean);
}
}
3、SpringSecurity配置
在
configure
方法中配置失败处理器
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
//3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
//4、异常处理
http
.exceptionHandling()
//认证失败处理器
.authenticationEntryPoint(myAuthenticationEntryPoint)
//权限不足处理器
.accessDeniedHandler(myAccessDeniedHandler);
}
}
八、跨域
1、编写配置类
/**
* 解决跨域问题
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
//允许任何域名
.allowedOriginPatterns("*")
//允许任何方法
.allowedMethods("PUT", "DELETE", "GET", "POST", "OPTIONS")
//允许任何头
.allowedHeaders("*")
//暴露头
.exposedHeaders("access-control-allow-headers",
"access-control-allow-methods",
"access-control-allow-origin",
"access-control-max-age",
"X-Frame-Options")
// 是否允许证书(cookies)
.allowCredentials(true)
.maxAge(3600);
}
}
2、在SpringSecurity配置类中配置
在配置类的configure()方法中开启允许跨域
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyOncePerRequestFilter myOncePerRequestFilter;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//1、关闭csrf,关闭Session
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//2、设置不需要认证的URL
http
.authorizeRequests()
//允许未登录的用户进行访问
.antMatchers("/doLogin").anonymous()
//其余url都要认证才能访问
.anyRequest().authenticated();
//3、在UsernamePasswordAuthenticationFilter前添加认证过滤器
http.addFilterBefore(myOncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
//4、异常处理
http
.exceptionHandling()
//认证失败处理器
.authenticationEntryPoint(myAuthenticationEntryPoint)
//权限不足处理器
.accessDeniedHandler(myAccessDeniedHandler);
//5、允许跨域
http.cors();
}
}