0


SpringSecurity前后端分离

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();}}

本文转载自: https://blog.csdn.net/X_lsod/article/details/122914659
版权归原作者 老子大威天龙 所有, 如有侵权,请联系我们删除。

“SpringSecurity前后端分离”的评论:

还没有评论