栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

SpringSecurity前后端分离

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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、导入依赖

    
        org.springframework.boot
        spring-boot-starter-web
    

    
        org.projectlombok
        lombok
        true
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    
    
    
        org.springframework.boot
        spring-boot-starter-security
    
    
        org.springframework.security
        spring-security-test
        test
    

    
    
        com.baomidou
        mybatis-plus-boot-starter
        3.4.1
    

    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    

    
    
        com.alibaba
        fastjson
        1.2.74
    

    
    
        cn.hutool
        hutool-all
        5.5.6
    
    
    
        com.baomidou
        mybatis-plus-generator
        3.4.1
    
    
    
        org.apache.commons
        commons-lang3
        3.7
    
    
    
        org.apache.velocity
        velocity-engine-core
        2.2
    
    
    
        io.springfox
        springfox-swagger-ui
        2.7.0
    

    
        io.springfox
        springfox-swagger2
        2.7.0
    

    
    
        io.jsonwebtoken
        jjwt
        0.9.0
    

    
    
        mysql
        mysql-connector-java
        runtime
    

    
    
        com.oracle.database.jdbc
        ojdbc8
        runtime
    


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;


public class CodeGenerator {

    
    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:WorkSpaceJavaWorkSpceidealAPP-shareimageAPP-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 {
    
    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;
    }

    
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

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

    
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    
    public static SecretKey generalKey() {
        byte[] encodedKey = base64.getDecoder().decode(SECRET_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    
    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;

    
    public  void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    
    public  void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    
    public  T getCacheObject(final String key)
    {
        ValueOperations operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    
    public  long setCacheList(final String key, final List dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    
    public  List getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    
    public  BoundSetOperations setCacheSet(final String key, final Set dataSet)
    {
        BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
        Iterator it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    
    public  Set getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    
    public  void setCacheMap(final String key, final Map dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    
    public  Map getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    
    public  void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    
    public  T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    
    public  List getMultiCacheMapValue(final String key, final Collection hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    
    public Collection 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;


@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate 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 implements RedisSerializer
{

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class 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、拓展接口

使公用返回对象枚举类和自定义异常方便扩展

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,"服务器响应错误!"),

    
    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,"请勿重复点赞"),



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

    
    @ExceptionHandler(value = ServicesException.class)
    public RespBean servicesExceptionHandler(ServicesException e){
        logger.error("发生业务异常! 原因是:{}",e.getMsg());
        return new RespBean(e);
    }


    
    @ExceptionHandler(value = Exception.class)
    public RespBean exceptionHandler(Exception e){
        logger.error("未知异常! 原因是:",e);
        return new RespBean();
    }


    
    @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
@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;


    
    @Override
    public RespBean doLogin(LoginDTO loginDTO) {

        
        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 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 getUserByUserName(String username);

}
1.2.4、TUserServiceImpl

TUserMapper需要继承baseMapper才能使用selectOne()这个方法

@Service
public class TUserServiceImpl extends ServiceImpl implements TUserService {

    @Value("${role.roleid}")
    private Integer roleId;

    @Autowired
    private TUserMapper userMapper;


    
    @Override
    public TUser getUserByUserName(String username) {
        LambdaQueryWrapper 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 接口
@Data
@AllArgsConstructor  //全参构造
@NoArgsConstructor  //无参构造
public class AuthUser implements UserDetails {

    private TUser tUser;

//    @JSonField(serialize = false)
    private Collection authorities;

    @Override
    public Collection 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对象

@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;

    
    @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 roles = roleService.getRolesByUserName(username);
            List 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 {

}
2、Token 认证模块 2.1、认证过滤器
@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);

    }
}

六、授权

使用 @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文件中定义的,可以自行修改

七、自定义失败处理器 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();
    }
}
转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号