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 代码生成器
*/publicclassCodeGenerator{/**
* <p>
* 读取控制台内容
* </p>
*/publicstatic String scanner(String tip){
Scanner scanner =newScanner(System.in);
StringBuilder help =newStringBuilder();
help.append("请输入"+ tip +":");
System.out.println(help.toString());if(scanner.hasNext()){
String ipt = scanner.next();if(StringUtils.isNotEmpty(ipt)){return ipt;}}thrownewMybatisPlusException("请输入正确的"+ tip +"!");}publicstaticvoidmain(String[] args){// 创建代码生成器对象
AutoGenerator mpg =newAutoGenerator();// 全局配置
GlobalConfig gc =newGlobalConfig();
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 =newDataSourceConfig();
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 =newPackageConfig();//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 =newStrategyConfig();//设置哪些表需要自动生成
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:8080spring:# 数据库配置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# 最大连接数,默认10maximum-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
#数据库,默认是0database:0#超时时间timeout: 1209600000ms
lettuce:pool:#最大链接数,默认8max-active:8#最大连接阻塞等待时间,默认-1max-wait: 10000ms
#最大空闲连接,默认8max-idle:200#最小空闲连接,默认0min-idle:5mybatis-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)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@Autowiredprivate MyOncePerRequestFilter myOncePerRequestFilter;@Bean@Overridepublic AuthenticationManager authenticationManagerBean()throws Exception {returnsuper.authenticationManagerBean();}@Overrideprotectedvoidconfigure(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格式返回配置类
publicabstractclassJSONAuthentication{/**
* 输出JSON
*
* @param request
* @param response
* @param obj
* @throws IOException
* @throws ServletException
*/protectedvoidWriteJSON(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、密码编码类
@ComponentpublicclassBCryptPasswordEncoderUtilextendsBCryptPasswordEncoder{@Overridepublic String encode(CharSequence rawPassword){returnsuper.encode(rawPassword);}@Overridepublicbooleanmatches(CharSequence rawPassword, String encodedPassword){returnsuper.matches(rawPassword,encodedPassword);}}
5.4、JWT工具类
@ComponentpublicclassJwtUtil{privatestaticfinal Logger logger = LoggerFactory.getLogger(JwtUtil.class);privatestatic String SECRET_KEY;privatestatic Long EXPIRATION_TIME;//对于静态变量,需要使用set方法才能使用设置好的字段值@Value("${jwt.secret}")publicvoidsetSECRET_KEY(String SECRET_KEY){this.SECRET_KEY = SECRET_KEY;}@Value("${jwt.expiration}")publicvoidsetEXPIRATION_TIME(Long expiration){this.EXPIRATION_TIME = expiration;}publicstatic String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-","");return token;}/**
* 生成jtw
*
* @param subject token中要存放的数据(json格式)
* @return
*/publicstatic String createJWT(String subject){
JwtBuilder builder =getJwtBuilder(subject, null,getUUID());// 设置过期时间return builder.compact();}/**
* 生成jtw
*
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/publicstatic String createJWT(String subject, Long ttlMillis){
JwtBuilder builder =getJwtBuilder(subject, ttlMillis,getUUID());// 设置过期时间return builder.compact();}privatestatic JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid){
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey =generalKey();long nowMillis = System.currentTimeMillis();
Date now =newDate(nowMillis);if(ttlMillis == null){
ttlMillis = EXPIRATION_TIME;}long expMillis = nowMillis + ttlMillis;
Date expDate =newDate(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
*/publicstatic String createJWT(String id, String subject, Long ttlMillis){
JwtBuilder builder =getJwtBuilder(subject, ttlMillis, id);// 设置过期时间return builder.compact();}/**
* 生成加密后的秘钥 secretKey
*
* @return
*/publicstatic SecretKey generalKey(){byte[] encodedKey = Base64.getDecoder().decode(SECRET_KEY);
SecretKey key =newSecretKeySpec(encodedKey,0, encodedKey.length,"AES");return key;}/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/publicstatic Claims parseJWT(String jwt)throws Exception {
SecretKey secretKey =generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}}
5.5、Redis工具类
@SuppressWarnings(value ={"unchecked","rawtypes"})@ComponentpublicclassRedisCache{@Autowiredpublic RedisTemplate redisTemplate;/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/public<T>voidsetCacheObject(final String key,final T value){
redisTemplate.opsForValue().set(key, value);}/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/public<T>voidsetCacheObject(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=设置失败
*/publicbooleanexpire(final String key,finallong timeout){returnexpire(key, timeout, TimeUnit.SECONDS);}/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/publicbooleanexpire(final String key,finallong 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
*/publicbooleandeleteObject(final String key){return redisTemplate.delete(key);}/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/publiclongdeleteObject(final Collection collection){return redisTemplate.delete(collection);}/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/public<T>longsetCacheList(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>voidsetCacheMap(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>voidsetCacheMapValue(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
*/publicvoiddelCacheMapValue(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
*/@ConfigurationpublicclassRedisConfig{@Bean@SuppressWarnings(value ={"unchecked","rawtypes"})public RedisTemplate<Object, Object>redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object, Object> template =newRedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer serializer =newFastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(newStringRedisSerializer());
template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(newStringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();return template;}}
5.7、序列化工具
publicclassFastJsonRedisSerializer<T>implementsRedisSerializer<T>{publicstaticfinal Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}publicFastJsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublicbyte[]serialize(T t)throws SerializationException
{if(t == null){returnnewbyte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes)throws SerializationException
{if(bytes == null || bytes.length <=0){return null;}
String str =newString(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 返回对象的接口,装饰者模式
*/publicinterfaceCommonResp{
Integer getCode();
String getMsg();
CommonResp setMsg(String msg);}
1.2、公用返回对象
@DatapublicclassRespBeanimplementsSerializable{privatestaticfinallong serialVersionUID =1L;private Integer code;private String msg;private Object obj;publicRespBean(RespBeanEnum respBeanEnum, Object obj){this.code = respBeanEnum.getCode();this.msg = respBeanEnum.getMsg();this.obj = obj;}publicRespBean(RespBeanEnum respBeanEnum){this.code = respBeanEnum.getCode();this.msg = respBeanEnum.getMsg();}publicRespBean(RespBeanEnum respBeanEnum, String msg){this.code = respBeanEnum.getCode();this.msg = msg;}publicRespBean(){this.code = RespBeanEnum.ERROR.getCode();this.msg = RespBeanEnum.ERROR.getMsg();}publicRespBean(String msg){this.code = RespBeanEnum.ERROR.getCode();this.msg = msg;}//自定义的业务异常错误码和信息publicRespBean(ServicesException e){this.code = e.getCode();this.msg = e.getMsg();}}
1.3、枚举类
publicenum RespBeanEnum implementsCommonResp{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;}@Overridepublic Integer getCode(){returnthis.code;}@Overridepublic String getMsg(){returnthis.msg;}@Overridepublic CommonResp setMsg(String msg){this.msg=msg;returnthis;}}
2、全局异常
2.1、自定义异常
实现
CommonResp接口,方便自定义异常后续修改错误信息
publicclassServicesExceptionextendsRuntimeExceptionimplementsCommonResp{private CommonResp commonResp;//直接接收RespBeanEnum的传参用于构造业务异常publicServicesException(CommonResp commonResp){super();//调用父类的无参构造方法this.commonResp = commonResp;}//接收自定义msg的方式构造业务异常publicServicesException(String msg, CommonResp commonResp){super();this.commonResp = commonResp;this.commonResp.setMsg(msg);}@Overridepublic Integer getCode(){returnthis.commonResp.getCode();}@Overridepublic String getMsg(){returnthis.commonResp.getMsg();}@Overridepublic CommonResp setMsg(String msg){this.commonResp.setMsg(msg);returnthis;}}
2.2、全局异常处理器
@RestControllerAdvice注解表示捕获控制层抛出的异常
@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常

(图片来源:https://blog.csdn.net/weixin_43702146/article/details/118606502)
因为使用了
@RestControllerAdvice注解,自动去捕获控制层抛出的异常,
AuthenticationException异常和
AccessDeniedException异常也被捕获了,但是我不想在这里处理,所以将这两个异常往外抛给失败处理器去处理。
@RestControllerAdvice//捕获controller层的异常publicclassGlobalExceptionHandler{privatestaticfinal 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());returnnewRespBean(e);}/**
* @Author: LZDWTL
* @param: [e]
* @return: com.guet.shareimage.response.RespBean
* @Description: 其他异常
*/@ExceptionHandler(value = Exception.class)public RespBean exceptionHandler(Exception e){
logger.error("未知异常! 原因是:",e);returnnewRespBean();}/**
* @Author: LZDWTL
* @Date: 2022/2/11
* @param: [authException]
* @return: void
* @Description: 将 AuthenticationException 异常往上抛,让认证处理器去处理
*/@ExceptionHandler(value = AuthenticationException.class)publicvoidaccountExpiredExceptionHandler(AuthenticationException authException){throw authException;}//将 AccessDeniedException 异常往上抛,让授权处理器去处理@ExceptionHandler(value = AccessDeniedException.class)publicvoidaccessDeniedExceptionHandler(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方式才能进行登陆
*/@RestControllerpublicclassLoginController{@Autowiredprivate 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
publicinterfaceLoginService{
RespBean doLogin(LoginDTO loginDTO);
RespBean doLogout();}
1.2.2、
LoginServiceImpl
这里的
AuthenticationManager需要在
SpringSecurity中使用
authenticationManagerBean()方法才能调用
AuthenticationManager的
authenticate()方法进行认证,抽象类
AbstractUserDetailsAuthenticationProvider中重写了
authenticate()方法
这里把生成的Token和查询到的用户信息存到Redis中,方便后续使用
@ServicepublicclassLoginServiceImplimplementsLoginService{privatestaticfinal Logger logger = LoggerFactory.getLogger(LoginServiceImpl.class);@Value("${jwt.tokenHead}")private String tokenHead;@Autowiredprivate TUserService userService;@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate UserDetailsService userDetailsService;@Autowiredprivate BCryptPasswordEncoderUtil passwordEncoder;@Autowiredprivate RedisCache redisCache;/**
* @Author: LZDWTL
* @param: [username, password]
* @return: com.guet.APPshareimage.response.RespBean
* @Description: 登陆
*/@Overridepublic RespBean doLogin(LoginDTO loginDTO){/**
* 因为我使用了全局异常处理,GobalExceptionHandler会自动捕获controller层抛出的异常
* authenticationManager.authenticate 这一句认证失败会抛出AuthenticationException异常
* 我定义了认证失败处理器无法获取到 AuthenticationException 异常,因为全局异常处理已经捕获了
* 然后 AuthenticationException 异常不属于 ServicesException,所以会返回500,服务器响应错误
*/
UsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);if(Objects.isNull(authenticate)){//用户名密码错误thrownewServicesException(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 =newHashMap<>();
map.put("token", token);returnnewRespBean(RespBeanEnum.SUCCESS, map);}@Overridepublic 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();returnnewRespBean(RespBeanEnum.SUCCESS);}}

1.2.3、
TUserService
publicinterfaceTUserServiceextendsIService<TUser>{
TUser getUserByUserName(String username);}
1.2.4、
TUserServiceImpl
TUserMapper需要继承
BaseMapper才能使用
selectOne()这个方法
@ServicepublicclassTUserServiceImplextendsServiceImpl<TUserMapper, TUser>implementsTUserService{@Value("${role.roleid}")private Integer roleId;@Autowiredprivate TUserMapper userMapper;/**
* @Author: LZDWTL
* @Date: 2021/12/28
* @param: [username]
* @return: com.guet.response.RespBean
* @Description: 通过用户名获取用户
*/@Overridepublic TUser getUserByUserName(String username){
LambdaQueryWrapper<TUser> lambdaQueryWrapper =newLambdaQueryWrapper<>();//查询条件:全匹配账号名,和状态为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//无参构造publicclassAuthUserimplementsUserDetails{private TUser tUser;// @JSONField(serialize = false)private Collection<?extendsGrantedAuthority> authorities;@Overridepublic Collection<?extendsGrantedAuthority>getAuthorities(){return authorities;}@Overridepublic String getPassword(){return tUser.getPassword();}@Overridepublic String getUsername(){return tUser.getUsername();}// 账户是否未过期@OverridepublicbooleanisAccountNonExpired(){returntrue;}// 账户是否未被锁@OverridepublicbooleanisAccountNonLocked(){returntrue;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){returntrue;}}
1.4、实现
UserDetailsService
接口
重写
UserDetailsService接口的
loadUserByUsername()方法,在
loadUserByUsername()方法中,会查询用户和权限(这里没有权限表,所以查的是角色),然后返回
UserDetails对象
/**
* 要实现UserDetailsService接口,这个接口是security提供的
*/@Service(value ="userDetailsService")publicclassAuthUserDetailsServiceImplimplementsUserDetailsService{privatestaticfinal Logger logger = LoggerFactory.getLogger(AuthUserDetailsServiceImpl.class);@Autowiredprivate TUserService userService;@Autowiredprivate TRoleService roleService;/**
* 通过账号查找用户、角色的信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/@Overridepublic UserDetails loadUserByUsername(String username)throws UsernameNotFoundException {
TUser user = userService.getUserByUserName(username);if(user == null){//用户名不存在thrownewServicesException(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);}else{//查找角色,实际应该查询权限,但我数据库没有设计所以就查角色就好了
List<String> roles = roleService.getRolesByUserName(username);
List<SimpleGrantedAuthority> authorities =newArrayList<>();for(String role : roles){
authorities.add(newSimpleGrantedAuthority(role));}
System.out.println("AuthUserDetailsServiceImpl-loadUserByUsername......user ===> "+ user);returnnewAuthUser(user, authorities);}}}
1.5、Mapper
1.5.1、
TUserMapper
@MapperpublicinterfaceTUserMapperextendsBaseMapper<TUser>{}
2、Token 认证模块
2.1、认证过滤器
/**
* @Author LZDWTL
* @Date 2021-12-20 16:28
* @ClassName ${MyOncePerRequestFilter}
* @Description ${认证过滤器}
*/@ComponentpublicclassMyOncePerRequestFilterextendsOncePerRequestFilter{privatestaticfinal Logger logger = LoggerFactory.getLogger(MyOncePerRequestFilter.class);@Value("${jwt.tokenHeader}")private String header;@Autowiredprivate RedisCache redisCache;@OverrideprotectedvoiddoFilterInternal(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,newRespBean(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);//输出JSONWriteJSON(request,response,newRespBean(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,newRespBean(RespBeanEnum.USER_NOT_LOGIN));return;}
UsernamePasswordAuthenticationToken authenticationToken =newUsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);}}
chain.doFilter(request, response);}privatevoidWriteJSON(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)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@Autowiredprivate MyOncePerRequestFilter myOncePerRequestFilter;@Bean@Overridepublic AuthenticationManager authenticationManagerBean()throws Exception {returnsuper.authenticationManagerBean();}@Overrideprotectedvoidconfigure(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")publicclassTUserController{@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")publicclassMyRBACService{publicbooleanhasPermission(HttpServletRequest request, Authentication authentication){
Object principal = authentication.getPrincipal();if(principal instanceofUserDetails){
UserDetails userDetails=(UserDetails)principal;/**
* 该方法主要对比认证过的用户是否具有请求URL的权限,有则返回true
*///本次要访问的资源
SimpleGrantedAuthority simpleGrantedAuthority=newSimpleGrantedAuthority(request.getRequestURI());//用户拥有的权限中是否包含请求的urlreturn userDetails.getAuthorities().contains(simpleGrantedAuthority);}returnfalse;}}
在SpringSecurity配置类中设置鉴权规则
@Overrideprotectedvoidconfigure(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格式,同时在里面判断是什么异常做针对性输出
@ComponentpublicclassMyAuthenticationEntryPointextendsJSONAuthenticationimplementsAuthenticationEntryPoint{privatestaticfinal Logger logger = LoggerFactory.getLogger(MyAuthenticationEntryPoint.class);@Overridepublicvoidcommence(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 instanceofAccountExpiredException){//账号过期
respBean =newRespBean(RespBeanEnum.USER_ACCOUNT_EXPIRED);}elseif(authException instanceofInternalAuthenticationServiceException){//用户不存在
respBean =newRespBean(RespBeanEnum.USER_ACCOUNT_NOT_EXIST);}elseif(authException instanceofBadCredentialsException){//用户名或密码错误(也就是用户名匹配不上密码)
respBean =newRespBean(RespBeanEnum.USERNAME_PASSWORD_ERROR);}elseif(authException instanceofCredentialsExpiredException){//密码过期
respBean =newRespBean(RespBeanEnum.USER_PASSWORD_EXPIRED);}elseif(authException instanceofDisabledException){//账号不可用
respBean =newRespBean(RespBeanEnum.USER_ACCOUNT_DISABLE);}elseif(authException instanceofLockedException){//账号锁定
respBean =newRespBean(RespBeanEnum.USER_ACCOUNT_LOCKED);}else{//其他错误
respBean =newRespBean(RespBeanEnum.USER_NOT_LOGIN);}//打印错误
logger.error(String.valueOf(authException));//输出this.WriteJSON(request, response, respBean);}}
2、权限不足处理器
@ComponentpublicclassMyAccessDeniedHandlerextendsJSONAuthenticationimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)throws IOException, ServletException {//用户权限不足
RespBean respBean =newRespBean(RespBeanEnum.USER_NO_PERMISSIONS);//输出this.WriteJSON(request, response, respBean);}}
3、SpringSecurity配置
在
configure方法中配置失败处理器
@Configuration@EnableGlobalMethodSecurity(prePostEnabled =true)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@Autowiredprivate MyOncePerRequestFilter myOncePerRequestFilter;@Autowiredprivate MyAuthenticationEntryPoint myAuthenticationEntryPoint;@Autowiredprivate MyAccessDeniedHandler myAccessDeniedHandler;@Bean@Overridepublic AuthenticationManager authenticationManagerBean()throws Exception {returnsuper.authenticationManagerBean();}@Overrideprotectedvoidconfigure(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、编写配置类
/**
* 解决跨域问题
*/@ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddCorsMappings(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)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@Autowiredprivate MyOncePerRequestFilter myOncePerRequestFilter;@Autowiredprivate MyAuthenticationEntryPoint myAuthenticationEntryPoint;@Autowiredprivate MyAccessDeniedHandler myAccessDeniedHandler;@Bean@Overridepublic AuthenticationManager authenticationManagerBean()throws Exception {returnsuper.authenticationManagerBean();}@Overrideprotectedvoidconfigure(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();}}
版权归原作者 老子大威天龙 所有, 如有侵权,请联系我们删除。