SpringBoot-零基础搭建前后端分离--后端搭建

SpringBoot-零基础搭建前后端分离–后端搭建

1.创建父项目verse

  1. 点击Create New Project

SpringBoot-零基础搭建前后端分离--后端搭建_第1张图片

  1. 选择 Maven ,选择本地安装的JDK, 点击 Next

SpringBoot-零基础搭建前后端分离--后端搭建_第2张图片

  1. 输入GroupID: com.verse 、ArtiactID:verse 点击 Finish

SpringBoot-零基础搭建前后端分离--后端搭建_第3张图片

  1. 创建完成后,删除src

SpringBoot-零基础搭建前后端分离--后端搭建_第4张图片

  1. pom.xml中添加依赖管理


    4.0.0

    com.verse
    verse
    1.0.0
    前后端分离verse
    
        1.8
        UTF-8
        UTF-8
        1.8
        1.8
        2.5.13
        3.5.1
        5.5.7
        3.0.0
    
    
    


    
        
            
                org.springframework.boot
                spring-boot-starter-parent
                ${spring.parent}
                pom
                import
            
            
            
                com.baomidou
                mybatis-plus-boot-starter
                ${mybatis-plus-version}
            
            
            
                cn.hutool
                hutool-all
                ${hutool.all.version}
            
            
            
                io.springfox
                springfox-boot-starter
                ${swagger.version}
            

        
    

    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    ${maven.compiler.source}
                    ${maven.compiler.target}
                
            
        
    


2.创建verse-commons

概述

通用异常处理以及通用响应数据结构等内容

新建verse-commons  Module

  1. 右击 verse模块名,点击 New > Module

SpringBoot-零基础搭建前后端分离--后端搭建_第5张图片

  1. 选择 Maven ,选择本地安装的JDK, 点击 Next

SpringBoot-零基础搭建前后端分离--后端搭建_第6张图片

  1. 输入GroupID: com.verse.commons、ArtiactID:verse-commons 点击 Finish

SpringBoot-零基础搭建前后端分离--后端搭建_第7张图片

  1. 修改verse-commons的pom.xml


    
        verse
        com.verse
        1.0.0
    
    4.0.0
    com.verse.commons
    verse-commons
    
        
        
            org.projectlombok
            lombok
            true
            provided
        
        
        
            com.fasterxml.jackson.core
            jackson-annotations
        
        
        
            org.springframework
            spring-web
            provided
        
    


添加统一返回结果

创建返回码接口IResultCode

/**
 * 返回码接口
 */
public interface IResultCode {

    /**
     * 返回码
     *
     * @return int
     */
    int getCode();

    /**
     * 返回消息
     *
     * @return String
     */
    String getMsg();
}

创建返回接口码的实现类ResultCode

package com.verse.commons.api;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 返回码实现
 */
@Getter
@AllArgsConstructor
public enum ResultCode implements IResultCode {

    /**
     * 操作成功
     */
    SUCCESS(200, "操作成功"),
    /**
     * 业务异常
     */
    FAILURE(400, "业务异常"),

    /**
     * 业务异常
     */
    Unauthorized(401, "用户、密码输入错误"),
    /**
     * 服务未找到
     */
    NOT_FOUND(404, "服务未找到"),
    /**
     * 服务异常
     */
    ERROR(500, "服务异常"),
    USER_INPUT_ERROR(400,"您输入的数据格式错误或您没有权限访问资源!"),
    /**
     * Too Many Requests
     */
    TOO_MANY_REQUESTS(429, "Too Many Requests");



    /**
     * 状态码
     */
    final int code;
    /**
     * 消息内容
     */
    final String msg;
}

创建 统一响应消息报文Result类

/**
 * 统一响应消息报文
 * @param 
 */
@Data
@Getter
public class Result implements Serializable {

    private static final long serialVersionUID = 1L;

    private int code;

    private String msg;


    private long time;


    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;

    private Result() {
        this.time = System.currentTimeMillis();
    }

    private Result(IResultCode resultCode) {
        this(resultCode, null, resultCode.getMsg());
    }

    private Result(IResultCode resultCode, String msg) {
        this(resultCode, null, msg);
    }

    private Result(IResultCode resultCode, T data) {
        this(resultCode, data, resultCode.getMsg());
    }

    private Result(IResultCode resultCode, T data, String msg) {
        this(resultCode.getCode(), data, msg);
    }

    private Result(int code, T data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
        this.time = System.currentTimeMillis();
    }

    /**
     * 返回状态码
     *
     * @param resultCode 状态码
     * @param         泛型标识
     * @return ApiResult
     */
    public static  Result success(IResultCode resultCode) {
        return new Result<>(resultCode);
    }

    public static  Result success(String msg) {
        return new Result<>(ResultCode.SUCCESS, msg);
    }

    public static  Result success(IResultCode resultCode, String msg) {
        return new Result<>(resultCode, msg);
    }

    public static  Result data(T data) {
        return data(data, VerseConstant.DEFAULT_SUCCESS_MESSAGE);
    }

    public static  Result data(T data, String msg) {
        return data(ResultCode.SUCCESS.code, data, msg);
    }

    public static  Result data(int code, T data, String msg) {
        return new Result<>(code, data, data == null ? VerseConstant.DEFAULT_NULL_MESSAGE : msg);
    }

    public static  Result fail() {
        return new Result<>(ResultCode.FAILURE, ResultCode.FAILURE.getMsg());
    }

    public static  Result fail(String msg) {
        return new Result<>(ResultCode.FAILURE, msg);
    }

    public static  Result fail(int code, String msg) {
        return new Result<>(code, null, msg);
    }

    public static  Result fail(IResultCode resultCode) {
        return new Result<>(resultCode);
    }

    public static  Result fail(IResultCode resultCode, String msg) {
        return new Result<>(resultCode, msg);
    }

    public static  Result condition(boolean flag) {
        return flag ? success(VerseConstant.DEFAULT_SUCCESS_MESSAGE) : fail(VerseConstant.DEFAULT_FAIL_MESSAGE);
    }
}

创建基础异常处理类BaseException

package com.verse.commons.exception;

import com.verse.commons.api.ResultCode;
import lombok.Data;
import org.springframework.http.HttpStatus;

import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;

/**
 * 基础异常处理类
 */
@Data
public class BaseException extends RuntimeException{

    private static final long serialVersionUID = 5782968730281544562L;

    private int status = INTERNAL_SERVER_ERROR.value();

    public BaseException(String message) {
        super(message);
    }

    public BaseException(HttpStatus status, String message) {
        super(message);
        this.status = status.value();
    }


    public BaseException(int code, String message) {
        super(message);
        this.code = code;
        this.message =message;
    }

    //异常错误编码
    private int code ;
    //异常信息
    private String message;

    private BaseException(){}

    public BaseException(ResultCode resultCode) {
        this.code = resultCode.getCode();
        this.message = resultCode.getMsg();
    }
}

创建verse基本常量

/**
 * verse基本常量
 */
public class VerseConstant {

    /**
     * 默认成功消息
     */
    public static final String DEFAULT_SUCCESS_MESSAGE = "处理成功";

    /**
     * 默认失败消息
     */
    public static final String DEFAULT_FAIL_MESSAGE = "处理失败";

    /**
     * 默认为空消息
     */
    public static final String DEFAULT_NULL_MESSAGE = "承载数据为空";
}

3.创建verse-jwt

概述

项目的后端核心服务(Spring Boot web应用)

新建verse-jwt   Module

  1. 右击 verse模块名,点击 New > Module

SpringBoot-零基础搭建前后端分离--后端搭建_第8张图片

  1. 选择 Maven ,选择本地安装的JDK, 点击 Next

SpringBoot-零基础搭建前后端分离--后端搭建_第9张图片

  1. 输入GroupID: com.verse.jwt、ArtiactID:verse-jwt 点击 Finish

SpringBoot-零基础搭建前后端分离--后端搭建_第10张图片

  1. 修改`verse-jwt的pom.xml


    
        verse
        com.verse
        1.0.0
    
    4.0.0
    com.verse.jwt
    verse-jwt

    
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            com.verse.commons
            verse-commons
            1.0.0
        
        
        
            mysql
            mysql-connector-java
            runtime
        
        
        
            com.baomidou
            mybatis-plus-boot-starter
        
        
        
            com.baomidou
            mybatis-plus-generator
        
        
        
            org.apache.velocity
            velocity
        
        
        
            io.springfox
            springfox-boot-starter
        
    



创建verse-jwt的入口类VerseJwtApplication

@MapperScan("com.verse.jwt.*.mapper")
@SpringBootApplication
public class VerseJwtApplication {
    public static void main(String[] args) {
        SpringApplication.run(VerseJwtApplication.class, args);
    }
}

4.在verse-jwt中实现代码生成

参考mybatis-plus代码生成:https://baomidou.com/pages/779a6e/

在verse-jwt中的pom.xml添加依赖

        
        
            com.baomidou
            mybatis-plus-generator
        

创建MybatisPlusGenerator类

  • DATA_SOURCE_CONFIG:数据链接配置

  • generator:根据模板生成代码

  • getTables方法:数据库表,all表示库中所有表

  • 将模板添加到src\main\resources\templates文件下

SpringBoot-零基础搭建前后端分离--后端搭建_第11张图片

public class MybatisPlusGenerator {

    private static final DataSourceConfig.Builder DATA_SOURCE_CONFIG = new DataSourceConfig
            .Builder("jdbc:mysql://localhost:3306/verse?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai", "root", "root");

    public static void generator(){
        FastAutoGenerator.create(DATA_SOURCE_CONFIG)
                // 全局配置
                .globalConfig(builder -> {
                    builder.author("springboot葵花宝典") // 设置作者
                            .enableSwagger() // 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir("D:\\software\\file\\verse"); // 指定输出目录
                })
                // 包配置
                .packageConfig((scanner, builder) -> builder.parent(scanner.apply("请输入包名?"))
                        .pathInfo(Collections.singletonMap(OutputFile.xml, "D:\\software\\file\\verse\\mapper"))

                )
                // 策略配置
                .strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all")))
                        .controllerBuilder().enableRestStyle().enableHyphenStyle()
                        .entityBuilder().enableLombok()
//                        .addTableFills(
//                                new Column("create_by", FieldFill.INSERT),
//                                new Column("create_time", FieldFill.INSERT),
//                                new Column("update_by", FieldFill.INSERT_UPDATE),
//                                new Column("update_time", FieldFill.INSERT_UPDATE)
//                        )
                        .build())
                /*
                    模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker
                   .templateEngine(new BeetlTemplateEngine())
                   .templateEngine(new FreemarkerTemplateEngine())
                 */
                .execute();
    }

    // 处理 all 情况
    protected static List getTables(String tables) {
        return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
    }
}

创建代码生成入口类GeneratorMain

public class GeneratorMain {
    public static void main(String[] args) {
        MybatisPlusGenerator.generator();
    }
}

运行GeneratorMain

图片

生成结果如下

SpringBoot-零基础搭建前后端分离--后端搭建_第12张图片

将system包放在verse-jwt的com.verse.jwt包下,结果如下:

SpringBoot-零基础搭建前后端分离--后端搭建_第13张图片

将mapper放在verse-jwtsrc\main\resources包下,结果如下:

SpringBoot-零基础搭建前后端分离--后端搭建_第14张图片

5.整合Swagger-ui实现在线API文档


添加项目依赖

verse-jwt项目的pom.xml中新增Swagger-UI相关依赖

        
        
            io.springfox
            springfox-boot-starter
        

添加Swagger-UI的配置

verse-jwt项目中添加如下类Swagger2Config:

package com.verse.jwt.config;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Configuration
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.verse.jwt.system.controller"))
                .paths(PathSelectors.any())
                .build()
                ;

    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("SwaggerUI演示")
                .description("verse")
                .contact(new Contact("springboot葵花宝典", null, null))
                .version("1.0")
                .build();
    }
    @Bean
    public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
        return new BeanPostProcessor() {

            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
                    customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
                }
                return bean;
            }

            private  void customizeSpringfoxHandlerMappings(List mappings) {
                List copy = mappings.stream()
                        .filter(mapping -> mapping.getPatternParser() == null)
                        .collect(Collectors.toList());
                mappings.clear();
                mappings.addAll(copy);
            }

            @SuppressWarnings("unchecked")
            private List getHandlerMappings(Object bean) {
                try {
                    Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
                    field.setAccessible(true);
                    return (List) field.get(bean);
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new IllegalStateException(e);
                }
            }
        };
    }
}

修改配置

修改application.yml文件,MVC默认的路径匹配策略为PATH_PATTERN_PARSER,需要修改为ANT_PATH_MATCHER

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  datasource:
    url: jdbc:mysql://localhost:3306/verse?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver


mybatis-plus:
  config-location:
    mapper-locations:
      - classpath:mapper/*.xml
      - classpath*:com/**/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

springfox:
  documentation:
    enabled: true
server:
  port: 8888

添加一个测试接口

  • 在ISysUserService接口中添加方法
import com.verse.jwt.system.entity.SysUser;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * 

 * 用户信息表 服务类  * 

 *  * @author springboot葵花宝典  * @since 2022-04-27  */ public interface ISysUserService extends IService {     SysUser getUserByUserName(String userName); }
  • 在SysUserServiceImpl中实现
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.verse.jwt.system.entity.SysUser;
import com.verse.jwt.system.mapper.SysUserMapper;
import com.verse.jwt.system.service.ISysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.annotation.Resource;

/**
 * 

 * 用户信息表 服务实现类  * 

 *  * @author springboot葵花宝典  * @since 2022-04-27  */ @Service public class SysUserServiceImpl extends ServiceImpl implements ISysUserService {     @Resource     private SysUserMapper sysUserMapper;     /**      * 根据登录用户名查询用户信息      * @param userName 用户信息      * @return      */     @Override     public SysUser getUserByUserName(String userName){         Assert.isTrue(StrUtil.isNotEmpty(userName),                 "查询参数用户名不存在");         SysUser sysUser = sysUserMapper.selectOne(                 new QueryWrapper().eq("username",userName));         if(sysUser != null){             sysUser.setPassword("");  //清空密码信息         }         return sysUser;     } }
  • 在SysUserController中实现接口
import com.verse.jwt.system.entity.SysUser;
import com.verse.jwt.system.service.ISysUserService;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 

 * 用户信息表 前端控制器  * 

 *  * @author springboot葵花宝典  * @since 2022-04-27  */ @RestController @Api(tags = "SysUserController", description =" 用户信息表") @RequestMapping("/sys-user") public class SysUserController {     @Resource     private ISysUserService sysuserService;     /**      * 根据登录用户名查询用户信息      * @param username 用户名称      * @return      */     @ApiOperation(value = "info")     @GetMapping(value = "/info")     public SysUser info(@RequestParam("username") String username) {         return sysuserService.getUserByUserName(username);     } }

测试

启动项目后,访问http://localhost:8888/swagger-ui/地址,结果如下

SpringBoot-零基础搭建前后端分离--后端搭建_第15张图片

6.整合SpringSecurity和JWT实现认证和授权


项目使用表说明

  • sys_user是用户信息表,用于存储用户的基本信息,如:用户名、密码

  • sys_role是角色信息表,用于存储系统内所有的角色

  • sys_menu是系统的菜单信息表,用于存储系统内所有的菜单。用id与父id的字段关系维护一个菜单树形结构。

  • sys_user_role是用户角色多对多关系表,一条userid与roleid的关系记录表示该用户具有该角色,该角色包含该用户。

  • sys_role_menu是角色菜单(权限)关系表,一条roleid与menuid的关系记录表示该角色由某菜单权限,该菜单权限可以被某角色访问。

  • sys_api,用于存储可以被访问的资源服务接口

  • sys_role_api,一条roleid与apiid的关系记录表示该角色具有某个api接口的访问权限。

添加项目依赖

  • verse-jtw的pom.xml中添加security项目依赖
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
        
            io.jsonwebtoken
            jjwt
        

添加Jwt属性配置类

  1. JwtProperties属性类
/**
 * jwt配置的属性
 */
@Data
@Component
@ConfigurationProperties("verse.jwt")
public class JwtProperties {
    //是否开启JWT,即注入相关的类对象
    private Boolean enabled;
    //JWT密钥
    private String secret;
    //JWT有效时间
    private Long expiration;
    //前端向后端传递JWT时使用HTTP的header名称
    private String header;
    //用户获取JWT令牌发送的用户名参数名称
    private String userParamName = "username";
    //用户获取JWT令牌发送的密码参数名称
    private String pwdParamName = "password";
    //允许哪些域对本服务的跨域请求
    private List corsAllowedOrigins;
    //允许哪些HTTP方法跨域
    private List corsAllowedMethods;
    //是否关闭csrf跨站攻击防御功能
    private Boolean csrfDisabled = true;
    //是否使用默认的JWTAuthController
    private Boolean useDefaultController = true;
}

  1. VerseApiProperties属性类

VerseApiProperties权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI

/**
 * 权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI
 */
@Data
@Component
@ConfigurationProperties(prefix = "verse.uaa")
public class VerseApiProperties {
    /**
     * 监控中心和swagger需要访问的url
     */
    public static final String[] ENDPOINTS = {
            "/jwtauth/**",
            "/swagger-ui/swagger-resources/**",
            "/swagger-resources/**",
            "/webjars/**",
            "/swagger-ui/**",
            "/v2/api-docs",
            "/v3/api-docs",
    };
    /**
     * 忽略URL,List列表形式
     */
    private List ignoreUrl = new ArrayList<>();

    /**
     * 首次加载合并ENDPOINTS
     */
    @PostConstruct
    public void initIgnoreUrl() {
        Collections.addAll(ignoreUrl, ENDPOINTS);
    }
}

  1. application.yml添加注解
verse:
  jwt:
    enabled: true
    secret: verse
    expiration: 3600000
    header: JWTHeaderName
    userParamName: username
    pwdParamName: password
    corsAllowedOrigins:
      - http://localhost:8080
      - http://127.0.0.1:8080
    corsAllowedMethods:
      - GET
      - POST
    useDefaultController: true
  uaa:
    ignoreUrl: #权限全面开放的接口,不需要JWT令牌就可以访问,或者开发过程临时开放的URI
      - /sys-user/info #根据用户名获取用户信息

添加JWT token的工具类

  1. 添加JWT token的工具类用于生成和解析JWT token的工具类

相关方法说明:

  • String generateToken(String username,Map payloads) :用于根据登录用户信息生成token

  • String getUsernameFromToken(String token):从token中获取登录用户的信息

  • Boolean validateToken(String token, String usernameParam):判断token是否还有效

/**
 * JwtToken生成工具类
 */
@Slf4j
@Component
public class JwtTokenUtil {

    @Autowired
    private JwtProperties jwtProperties;

    private static final String CLAIM_KEY_CREATED = "created";

    /**
     * 根据用户信息生成JWT的token令牌
     *
     * @param username 用户
     * @param payloads 令牌中携带的附加信息
     * @return 令token牌
     */
    public String generateToken(String username,
                                Map payloads) {
        int payloadSizes = payloads == null? 0 : payloads.size();

        Map claims = new HashMap<>(payloadSizes + 2);
        claims.put("sub", username);
        claims.put("created", new Date());

        if(payloadSizes > 0){
            for(Map.Entry entry:payloads.entrySet()){
                claims.put(entry.getKey(),entry.getValue());
            }
        }

        return generateToken(claims);
    }

    /**
     * 从token中获取JWT中的负载
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(jwtProperties.getSecret())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + + jwtProperties.getExpiration());
    }

    /**
     * 从token令牌中获取登录用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 验证token是否还有效
     *
     * @param token  客户端传入的token令牌
     * @param usernameParam 用户名的唯一标识
     * @return 是否有效
     */
    public Boolean validateToken(String token, String usernameParam) {
        //根据toekn获取用户名
        String username = getUsernameFromToken(token);
        return (username.equals(usernameParam) && !isTokenExpired(token));
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            //根据token获取获取过期时间
            Date expiration = getExpiredDateFromToken( token);
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据负责生成JWT的token
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map claims) {
        //生成token的过期时间
        Date expirationDate = generateExpirationDate();
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, jwtProperties.getSecret())
                .compact();
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            //获取负载信息
            Claims claims = getClaimsFromToken(token);
            claims.put(CLAIM_KEY_CREATED, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 从token令牌中获取登录用户名
     *
     * @return 用户名
     */
    public String getUsernameFromToken() {
        String username;
        try {
            RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
            HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
            String jwtToken = request.getHeader(jwtProperties.getHeader());

            Claims claims = getClaimsFromToken(jwtToken);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }
}

添加SpringSecurity的配置类

/**
 * Spring Security 配置
 * 可以配置多个WebSecurityConfigurerAdapter
 * 但是多个Adaptor有执行顺序,默认值是100
 * 这里设置为1会优先执行
 */
@Configuration
@Order(1)
public class JwtSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Resource
    private JwtProperties jwtProperties;

    @Resource
    private VerseApiProperties apiProperties;

    @Resource
    private MyUserDetailsService myUserDetailsService;

    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Resource
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Resource
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        if(jwtProperties.getCsrfDisabled()){
            http = http.csrf().disable()

            ;
        }
        http.cors()
                .and()
                .sessionManagement()// 基于token,所以不需要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                ;

        //通过配置实现的不需要JWT令牌就可以访问的接口
        for(String uri : apiProperties.getIgnoreUrl()){
            http.authorizeRequests().antMatchers(uri).permitAll();
        }
        //RBAC权限控制级别的接口权限校验
        http.authorizeRequests().anyRequest()
                .authenticated()
        //.access("@rabcService.hasPermission(request,authentication)")
        ;
        //添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) {
        //将项目中静态资源路径开放出来
        web.ignoring().antMatchers(apiProperties.ENDPOINTS);
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 跨站资源共享配置
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource() {

        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(jwtProperties.getCorsAllowedOrigins());
        configuration.setAllowedMethods(jwtProperties.getCorsAllowedMethods());
        configuration.applyPermitDefaultValues();

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Override
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public JwtAuthService jwtAuthService(JwtTokenUtil jwtTokenUtil) throws Exception {
        return new JwtAuthService(
                this.authenticationManagerBean(),jwtTokenUtil);
    }
}


相关依赖及方法说明

  • corsConfigurationSource: 跨域设置

  • configure(HttpSecurity httpSecurity):用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器;

  • configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;

  • RestfulAccessDeniedHandler:当用户没有访问权限时的处理器,用于返回JSON格式的处理结果;

  • RestAuthenticationEntryPoint:当未登录或token失效时,返回JSON格式的结果;

  • UserDetailsService:SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;

  • UserDetails:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现;

  • PasswordEncoder:SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder;

  • JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录

  • JwtAuthService:认证服务Service

添加RestfulAccessDeniedHandler

import cn.hutool.json.JSONUtil;
import com.verse.commons.api.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 当访问接口没有权限时,自定义的返回结果
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(Result.fail(e.getMessage())));
        response.getWriter().flush();
    }
}

添加RestAuthenticationEntryPoint

import cn.hutool.json.JSONUtil;
import com.verse.commons.api.Result;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 当未登录或者token失效访问接口时,自定义的返回结果
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(Result.fail(authException.getMessage())));
        response.getWriter().flush();
    }
}

添加MyUserDetailsServiceMapper

import com.verse.jwt.auth.dto.MyUserDetails;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * 用户信息Mapper
 */
public interface MyUserDetailsServiceMapper {

    //根据username查询用户信息
    @Select("SELECT username,password,enabled\n" +
            "FROM sys_user u\n" +
            "WHERE u.username = #{username}")
    MyUserDetails findByUserName(@Param("username") String username);

    //根据username查询用户角色列表
    @Select("SELECT role_code\n" +
            "FROM sys_role r\n" +
            "LEFT JOIN sys_user_role ur ON r.id = ur.role_id  AND r.status = 0\n" +
            "LEFT JOIN sys_user u ON u.id = ur.user_id\n" +
            "WHERE u.username = #{username}")
    List findRoleByUserName(@Param("username") String username);

    //根据用户角色查询用户菜单权限
    @Select({
            ""
    })
    List findMenuByRoleCodes(@Param("roleCodes") List roleCodes);
    //根据用户角色查询用户接口访问权限
    @Select(
            "SELECT url \n" +
                    "FROM sys_api a \n" +
                    "LEFT JOIN sys_role_api ra ON a.id = ra.api_id \n" +
                    "LEFT JOIN sys_role r ON r.id = ra.role_id \n" +
                    "WHERE r.role_code = #{roleCode} \n" +
                    "AND a.status = 0"
    )
    List findApiByRoleCode(@Param("roleCode") String roleCode);
}


添加MyUserDetails

public class MyUserDetails implements UserDetails {

    String password; //密码
    String username;  //用户名
    boolean accountNonExpired;   //是否没过期
    boolean accountNonLocked;   //是否没被锁定
    boolean credentialsNonExpired;  //是否没过期
    boolean enabled;  //账号是否可用
    Collection authorities;  //用户的权限集合

    @Override
    public Collection getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public void setAuthorities(Collection authorities) {
        this.authorities = authorities;
    }
}

添加JwtAuthService

JwtAuthService是认证服务的Service

/**
 * 认证登录服务
 */
public class JwtAuthService {

    private AuthenticationManager authenticationManager;
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    private MyUserDetailsServiceMapper myUserDetailsServiceMapper;

    private JwtAuthService(){}

    public JwtAuthService(AuthenticationManager authenticationManager,
                          JwtTokenUtil jwtTokenUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtTokenUtil = jwtTokenUtil;
    }

    /**
     * 登录认证换取JWT令牌
     * @return JWT
     */
    public String login(String username,
                        String password,
                        Map payloads) throws BaseException {
        try {
            UsernamePasswordAuthenticationToken upToken =
                    new UsernamePasswordAuthenticationToken(username, password);
            Authentication authentication = authenticationManager.authenticate(upToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }catch (AuthenticationException e){
            throw new BaseException(ResultCode.FAILURE.getCode()
                    ,"用户名或者密码输入错误,或者新建用户未赋予角色权限!");
        }

        return jwtTokenUtil.generateToken(username,payloads);
    }


    public String refreshToken(String oldToken){
        if(!jwtTokenUtil.isTokenExpired(oldToken)){
            return jwtTokenUtil.refreshToken(oldToken);
        }
        return null;
    }

    /**
     * 获取角色信息列表
     * @param token
     * @return
     */
    public List roles(String token){
        String username = jwtTokenUtil.getUsernameFromToken(token);
        //加载用户角色列表
        List roleCodes =
                myUserDetailsServiceMapper.findRoleByUserName(username);
        return roleCodes;
    }

}

添加MyRBACService

MyRBACService用户角色service

/**
 * 权限服务
 */
@Component("verseService")
public class MyRBACService {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Resource
    private VerseApiProperties verseApiProperties;

    @Resource
    private MyUserDetailsServiceMapper myUserDetailsServiceMapper;

    /**
     * 判断某用户是否具有该request资源的访问权限
     */
    public boolean hasPermission(HttpServletRequest request, Authentication authentication){

        Object principal = authentication.getPrincipal();

        if(principal instanceof UserDetails){

            UserDetails userDetails = ((UserDetails)principal);
            List authorityList =
                    AuthorityUtils.commaSeparatedStringToAuthorityList(request.getRequestURI());
            return userDetails.getAuthorities().contains(authorityList.get(0))
                    || verseApiProperties.getIgnoreUrl().contains(request.getRequestURI());
        }

        return false;
    }


    public MyUserDetails findByUserName(String username) {
        return myUserDetailsServiceMapper.findByUserName(username);
    }

    public List findRoleByUserName(String username) {
        return myUserDetailsServiceMapper.findRoleByUserName(username);
    }

    public List findApiByRoleCode(String roleCode) {
        return myUserDetailsServiceMapper.findApiByRoleCode(roleCode);
    }

}

添加MyUserDetailsService

@Component
public class MyUserDetailsService implements UserDetailsService {

    @Resource
    MyRBACService myRBACService;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {

        //加载基础用户信息
        MyUserDetails myUserDetails = myRBACService.findByUserName(username);

        //加载用户角色列表
        List roleCodes = myRBACService.findRoleByUserName(username);

        List authorities = new ArrayList<>();
        for(String roleCode : roleCodes){
            //通过用户角色列表加载用户的资源权限列表
            authorities.addAll(myRBACService.findApiByRoleCode(roleCode));
        }

        //角色是一个特殊的权限,ROLE_前缀
        roleCodes = roleCodes.stream()
                .map(rc -> "ROLE_" +rc)
                .collect(Collectors.toList());

        authorities.addAll(roleCodes);

        myUserDetails.setAuthorities(
                AuthorityUtils.commaSeparatedStringToAuthorityList(
                        String.join(",",authorities)
                )
        );
        return myUserDetails;
    }
}

添加JwtAuthenticationTokenFilter

在用户名和密码校验前添加的过滤器,如果请求中有jwt的token且有效,会取出token中的用户名,然后调用SpringSecurity的API进行登录操作。

/**
 * JWT令牌授权过滤器
 * 1.判断令牌的有效性
 * 2.根据令牌为该用户授权可以访问的资源
 */
@Slf4j
@Configuration
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private JwtProperties jwtProperties;
    private JwtTokenUtil jwtTokenUtil;
    private MyUserDetailsService myUserDetailsService;

    private JwtAuthenticationTokenFilter(){}

    public JwtAuthenticationTokenFilter(JwtProperties jwtProperties,
                                        JwtTokenUtil jwtTokenUtil,
                                        MyUserDetailsService myUserDetailsService) {
        this.jwtProperties = jwtProperties;
        this.jwtTokenUtil = jwtTokenUtil;
        this.myUserDetailsService = myUserDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        //获取token
        String authHeader = request.getHeader(jwtProperties.getHeader());
        if (!StrUtil.isEmpty(authHeader) ) {

            String username = jwtTokenUtil.getUsernameFromToken(authHeader);
            logger.info("checking username:"+ username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.myUserDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.validateToken(authHeader, username)) {
                    //给使用该JWT令牌的用户进行授权
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                    logger.info("authenticated user:"+username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

添加JwtAuthController

JwtAuthController主要用户获取token,刷新token

@RestController
@Api(tags = "JwtAuthController")
@RequestMapping("/jwtauth")
public class JwtAuthController {

    @Resource
    private JwtProperties jwtProperties;

    @Resource
    private JwtAuthService jwtAuthService;

    @Resource
    private MyUserDetailsServiceMapper myUserDetailsServiceMapper;


    /**
     * 使用用户名密码换JWT令牌
     */
    @ApiOperation(value = JWTConstants.CONTROLLER_AUTHENTICATION)
    @RequestMapping(value = JWTConstants.CONTROLLER_AUTHENTICATION)
    public Result login(@RequestBody Map map){

        String username  = map.get(jwtProperties.getUserParamName());
        String password = map.get(jwtProperties.getPwdParamName());

        if(StrUtil.isEmpty(username)
                || StrUtil.isEmpty(password)){
            return Result.fail(
                    ResultCode.Unauthorized,
                    "用户名或者密码不能为空");
        }
        try {
            return Result.data(
                    jwtAuthService.login(username, password,null));
        }catch (BaseException e){
            return Result.fail(ResultCode.FAILURE,e.getMessage());
        }
    }

    /**
     * 刷新JWT令牌
     */
    @ApiOperation(value = JWTConstants.CONTROLLER_REFRESH)
    @RequestMapping(value = JWTConstants.CONTROLLER_REFRESH)
    public  Result refresh(@RequestHeader("${verse.jwt.header}") String token){
        return Result.data(jwtAuthService.refreshToken(token));
    }


    /**
     * 获取用户角色列表接口
     */
    @ApiOperation(value = JWTConstants.CONTROLLER_ROLES)
    @RequestMapping(value = JWTConstants.CONTROLLER_ROLES)
    public  Result roles(
            @RequestHeader("${verse.jwt.header}") String token){
        return Result.data(jwtAuthService.roles(token));
    }

}


修改Swagger的配置

通过修改配置实现调用接口自带Authorization头,这样就可以访问需要登录的接口了。

package com.verse.jwt.config;

import io.swagger.annotations.Api;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Configuration
public class Swagger2Config {

    @Value("${verse.jwt.header}")
    private  String header ;


    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                //.apis(RequestHandlerSelectors.basePackage("com.verse.jwt.system.controller"))
                //为有@Api注解的Controller生成API文档
                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                .paths(PathSelectors.any())
                .build()
                //添加登录认证
                .securitySchemes(securitySchemes())
                .securityContexts(securityContexts())
                ;

    }


    private List securitySchemes() {
        //设置请求头信息
        List result = new ArrayList<>();
        ApiKey apiKey = new ApiKey("JWTVerseHeaderName", "JWTVerseHeaderName", "header");
        result.add(apiKey);
        return result;
    }

    private List securityContexts() {
        //设置需要登录认证的路径
        List result = new ArrayList<>();
        result.add(getContextByPath("/sys-role/.*"));
        return result;
    }

    private SecurityContext getContextByPath(String pathRegex) {
        return SecurityContext.builder()
                .securityReferences(defaultAuth())
                .forPaths(PathSelectors.regex(pathRegex))
                .build();
    }

    private List defaultAuth() {
        List result = new ArrayList<>();
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        result.add(new SecurityReference("JWTVerseHeaderName", authorizationScopes));
        return result;
    }



    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("SwaggerUI演示")
                .description("verse")
                .contact(new Contact("springboot葵花宝典", null, null))
                .version("1.0")
                .build();
    }
    @Bean
    public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
        return new BeanPostProcessor() {

            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
                    customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
                }
                return bean;
            }

            private  void customizeSpringfoxHandlerMappings(List mappings) {
                List copy = mappings.stream()
                        .filter(mapping -> mapping.getPatternParser() == null)
                        .collect(Collectors.toList());
                mappings.clear();
                mappings.addAll(copy);
            }

            @SuppressWarnings("unchecked")
            private List getHandlerMappings(Object bean) {
                try {
                    Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
                    field.setAccessible(true);
                    return (List) field.get(bean);
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    throw new IllegalStateException(e);
                }
            }
        };
    }
}


认证与授权流程演示

运行项目,访问API

Swagger api地址:http://localhost:8888/swagger-ui/

SpringBoot-零基础搭建前后端分离--后端搭建_第16张图片

免密登录访问接口

SpringBoot-零基础搭建前后端分离--后端搭建_第17张图片

未登录前访问接口

SpringBoot-零基础搭建前后端分离--后端搭建_第18张图片

获取token

SpringBoot-零基础搭建前后端分离--后端搭建_第19张图片

  • 点击Authorize按钮,在弹框中输入登录接口中获取到的token信息

SpringBoot-零基础搭建前后端分离--后端搭建_第20张图片

  • 登录后访问获取权限列表接口,发现已经可以正常访问

SpringBoot-零基础搭建前后端分离--后端搭建_第21张图片

如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!

SpringBoot-零基础搭建前后端分离--后端搭建_第22张图片

springboot葵花宝典

主要分享JAVA技术,主要包含SpringBoot、SpingCloud、Docker、中间件等技术,以及Github开源项目

93篇原创内容

公众号

原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!

你可能感兴趣的:(SpringSecurity,springsecurity,springboot,jwt,swagger)