SpringSecurity-JWT-微服务授权项目
Spring Security 微服务权限管理
Spring Security:作为一个基于Spring的优秀Web网站安全框架,除了能在单体的微服务中对权限进行拦截,我们还可以应用到微服务架构中。实现单点登录权限授权等功能。
在Spring Security中实现微服务的权限管理,最好结合 JWT
,那么接下来我们先了解一下什么是JWT
JWT(Json Web Token)
概述
Json web token (JWT)
, 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
起源
在Web安全认证中经常用到的就是传统的基于Session
的安全验证,和新崛起的JWT
传统的Session认证:
我们都知道Http
请求是一种无状态的请求协议,当用户发送请求时我们,并不知道是谁发送的请求,那么在传统的方案中,用户在第一次请求时就会要求用户就行授权登录,授权成功服务器就会将请求的用户信息保存在Session
中,并且给用户响应一个加密的Cookie
,下次访问时带上Cookie
,服务器通过解析Cookie
,来判断用户是否登录。但是基于Session的保存方式随之互联网的发展,会渐渐的出现很多问题。
Session认证 缺点
:
- Session的保存位置是在内存中,当网站的用户量增大时,服务器的大量内存都用来保存Session,那么提供资源的服务也会降低,速度减慢。
- 在如今的微服务架构中,Session是保存在一台服务器的内存中,然而互联网的发展,微服务集群,服务拆分都是常事,那么对用户在访问时不同服务器都需要授权。扩展能力差。
CSRF
存放在Cookies中,那么当Cookies被盗取时,很可能会受到黑客的伪装请求攻击,导致服务器宕机。
基于JWT的授权认证
基于JWT
的授权认证,他不需要服务器存放用户信息,而是将用户请求的信息保存到数据库或者(NoSQL数据库),推荐一款超级快的Redis
的NoSQL数据库。具体实现原理如下。
- 用户在第一次请求的时候,需要经行安全授权验证。
- 验证成功后服务器会将用户授权的信息利用
JWT
加密生成Token
, - 将
Token
响应给用户,并且将Token
保存到Redis中(可以是数据库,Redis基于内存更快)。 - 用户将接收到的
Token
保存起来(前端工程师完成)。 - 第二次请求时,只需要在请求头中携带
Token
,访问即可。 - 浏览器在接收到请求后,利用
Redis
,返回保存的Token
,并经行校验。校验成功就放行 - 利用
Redis
,可以解决多台服务器之间的授权问题,实现单点登录
(单点登录就是解决Session缺点的第二点的)
JWT组成
JWT的实际形式使用三部分组成,并通过 .
来分割
# 主要组成的形式
xxxxx.yyyyy.zzzzz
这三部分分别是Header
、Payload
、Signature
- Header(头),
typ
:属于什么类型的加密、alg
:加密方法 。通过对上面的定义在通过Base64Url
编码形成第一部分。
{
'typ': 'JWT'
'alg': 'HS256'
}
-
Payload(载荷),携带的参数,主要分为
sub
: 标准的声明name
:公共的声明admin
:私有的声明
通过对上面的定义在通过
Base64Url
编码形成第二部分。
{
'sub': '123456'
'name': 'john'
'admin': true
}
- Signature,是对上面两个加密的结果在经行加盐加密
- 简单来说就是利用上面两个部分的加密结果,在经行加盐加密,形成第三部分
var encodestring = base64UrlEncode(Header)+`.`+base64UrlEncode(payload)
var signature = HMACSHA256(encodestring, 'salt')
JWT的使用
-
构建一个
Maven
项目 -
导入以下依赖
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!--JDK 1.8 以上需要加入以下依赖--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>
-
测试代码
import io.jsonwebtoken.*; import java.util.Date; import java.util.UUID; public class JWT { public static void main(String[] args) { String salt = "salt"; // 加密的盐 JwtBuilder jwtBuilder = Jwts.builder(); String jwtToken = jwtBuilder // header 设置 .setHeaderParam("typ","JWT") .setHeaderParam("alg","HS256") // Payload 设置 .claim("username","tom") .claim("role","admin") .setSubject("admin-test") // 过期时间 .setExpiration(new Date(System.currentTimeMillis()+(1000*60*60*24))) // 设置 id .setId(UUID.randomUUID().toString()) // Signature 设置,主要是加密方式,和加盐参数 .signWith(SignatureAlgorithm.HS256,salt) // 拼接 .compact(); System.out.println(jwtToken); // 调用解析方法 JWT.jwtDecrypt(jwtToken,salt); } public static void jwtDecrypt(String encryptionPassword,String salt){ // 传入加盐的参数,和加密对象 Jws<Claims> claimsJws = Jwts.parser().setSigningKey(salt).parseClaimsJws(encryptionPassword); // 解析Body(Payload)部分的值 Claims body = claimsJws.getBody(); System.out.println(body.get("username")); System.out.println(body.get("role")); System.out.println(body.getId()); System.out.println(body.getExpiration()); // 解析头部的值 Header header = claimsJws.getHeader(); System.out.println(header.get("typ")); System.out.println(header.get("alg")); } }
基于Spring Security和JWT的微服务权限校验
数据库准备
-
在数据库终准备三个表,分别是
用户表
、角色表
、用户角色关系表
。 -
建表语句如下
CREATE TABLE `jwt_role` ( `id` char(19) NOT NULL DEFAULT '' COMMENT '角色id', `role_name` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名称', `role_code` varchar(20) DEFAULT NULL COMMENT '角色编码', `remark` varchar(255) DEFAULT NULL COMMENT '备注', `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `version` int(1) NOT NULL DEFAULT 1 COMMENT '版本控制,乐观锁', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `jwt_user` ( `id` char(19) NOT NULL COMMENT '用户id', `username` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名', `password` varchar(32) NOT NULL DEFAULT '' COMMENT '用户密码', `salt` varchar(100) NOT NULL DEFAULT '' COMMENT '密码加密盐', `nick_name` varchar(50) DEFAULT NULL COMMENT '昵称', `head_portrait` varchar(255) DEFAULT NULL COMMENT '用户头像', `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `version` int(1) NOT NULL DEFAULT 1 COMMENT '版本控制,乐观锁', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `jwt_user_role` ( `id` char(19) NOT NULL DEFAULT '' COMMENT '主键id', `role_id` char(19) NOT NULL DEFAULT '0' COMMENT '角色id', `user_id` char(19) NOT NULL DEFAULT '0' COMMENT '用户id', `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除', `version` int(1) NOT NULL DEFAULT 1 COMMENT '版本控制,乐观锁', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_role_id` (`role_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; insert into jwt_user(id, username, password, salt, nick_name, head_portrait, deleted, version, create_time, update_time) VALUES (1,'admin','adbc396f4cd1b6b1f41967bd8a857dcc','admin','橘子有点甜',null,0,1,now(),now()), (2,'admin1','adbc396f4cd1b6b1f41967bd8a857dcc','admin','橘子有点甜2',null,0,1,now(),now()) insert into jwt_role (id, role_name, role_code, remark, deleted, version, create_time, update_time) VALUES (1,'管理员','admin','可以管理所有模块',0,1,now(),now()),(2,'测试员','test','可以管理测试模块',0,1,now(),now()) insert into jwt_user_role (id, role_id, user_id, deleted, version, create_time, update_time) VALUES (1,1,1,0,1,now(),now()),(2,2,1,0,1,now(),now()),(3,2,2,0,1,now(),now())
注册中心准备(Nacos)
Nacos 如何安装和启动,参考如下教程:Nacos 安装启动教程
NoSQL数据库准备(Redis)
Redis如何安装启动,参考如下教程:Redis安装教程
安装完成,(如果是远程服务器)默认我们的项目是连接不了的,需要修改一些配置,参考如下地址修改配置后重新启动
https://www.cnblogs.com/swda/p/12013439.html
项目搭建
创建父工程
-
创建一个空的
Maven
项目 -
删除
src
目录 -
导入如下依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.wyx</groupId> <artifactId>SpringSecurity-JWT</artifactId> <packaging>pom</packaging> <version>1.0</version> <properties> <project.build.sourceEmcoding>UTF-8</project.build.sourceEmcoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <!--对应的版本--> <spring.boot.dependencies.version>2.4.3</spring.boot.dependencies.version> <spring.cloud.dependencies.version>2020.0.2</spring.cloud.dependencies.version> <spring.cloud.alibaba.dependencies.version>2.2.1.RELEASE</spring.cloud.alibaba.dependencies.version> <mysql.version>8.0.23</mysql.version> <log4j.version>1.2.17</log4j.version> <junit.version>4.13</junit.version> <lombok.version>1.18.20</lombok.version> <mybatis.plus.boot.starter.version>3.4.2</mybatis.plus.boot.starter.version> <mybatis.plus.generator.version>3.4.1</mybatis.plus.generator.version> <velocity.version>2.2</velocity.version> <jwt.version>0.9.1</jwt.version> <swagger.version>2.9.2</swagger.version> <hutool.version>5.7.2</hutool.version> </properties> <dependencyManagement> <dependencies> <!--SpringBoot 依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--Spring Cloud 依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud.dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--Spring Cloud Alibaba 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring.cloud.alibaba.dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--Mysql 连接驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--Lombok 依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <!--log4j 依赖--> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> <!--junit 单元测试 依赖--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> </dependency> <!--mybatis-plus整合SpringBoot依赖--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis.plus.boot.starter.version}</version> </dependency> <!--代码生成器依赖--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybatis.plus.generator.version}</version> </dependency> <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>${velocity.version}</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jwt.version}</version> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <!--swagger ui--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <!--HuTool工具包--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.4.4</version> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build> </project>
创建工具包API接口
-
创建新模块 common-api
-
导入pom.xml
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> </dependencies>
-
编写工具类
统一返回类型
package com.wyx.utils; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.HashMap; @Data @NoArgsConstructor @AllArgsConstructor public class CommonResult<T> { private Integer code; private String message; private T data; public static CommonResult Success(Object data){ return new CommonResult(200,"success",data); } public static CommonResult Error(){ HashMap<String, String> map = new HashMap<>(); map.put("warning",String.valueOf("请求出错,请联系管理员")); return new CommonResult(444,"error",map); } public static CommonResult isNull(){ HashMap<String, String> map = new HashMap<>(); map.put("warning",String.valueOf("请求数据为空,请联系管理员")); return new CommonResult(404,"error",map); } public static CommonResult perNoAll(){ HashMap<String, String> map = new HashMap<>(); map.put("message",String.valueOf("权限不允许,请联系管理员")); return new CommonResult(401,"warning",map); } public static CommonResult ErrorMethod(){ HashMap<String, String> map = new HashMap<>(); map.put("message",String.valueOf("请求方法错误,请查看请求方法")); return new CommonResult(403,"warning",map); } public static CommonResult Exception(){ HashMap<String, String> map = new HashMap<>(); map.put("message",String.valueOf("服务器内部错误,请联系管理员")); return new CommonResult(500,"error",map); } public static CommonResult MyMessage (String message){ HashMap<String, String> map = new HashMap<>(); map.put("message",message); return new CommonResult(911,"warning",map); } }
密码加密工具类
package com.wyx.utils; import cn.hutool.crypto.digest.DigestUtil; /** * 对传入的密码经行加密 * @ClassName UserPasswordEncryption * @Description TODO * @Author 王玉星 * @Date 2021/8/3 13:57 * @Version 1.0 */ public class PasswordEncryption { /** * 使用 MD5 方式对用户传入的密码经行加密 * @param password 用户传入的密码 * @param salt 加密的盐,最少要 4 位数 * * @return 使用用户密码加上加密盐,加密后的MD5值 */ public static String encryption(String password, String salt){ String saltPassword = salt.charAt(2)+salt.charAt(3)+password+salt.charAt(0)+salt.charAt(1); return DigestUtil.md5Hex(saltPassword); } }
JWT
加密解密类package com.wyx.utils; import io.jsonwebtoken.*; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; /** * 对用户信息利用JWT的方式设置加密解密功能 * @ClassName LoginToken * @Description None * @Author 王玉星 * @Date 2021/8/1 14:12 * @Version 1.0 */ public class TokenManager { //推荐用公司名 private static String SALT = "con.wyx"; // 过期时间 7 天 private static Integer EXPIRE_TIME = 1000*60*60*24*7; public static String encryption(Long id, String username){ return encryption(id,username,""); } /** * 加密用户信息函数 * @param id 用户Id * @param username 用户姓名 * @param role 用户角色,输入时用,隔开。代表多个角色,形如 "admin,user" * * @return 经过JWT加密后的密文 */ public static String encryption(Long id, String username, String role){ String[] roles = role.split(","); JwtBuilder jwtBuilder = Jwts.builder(); String jwtPassword = jwtBuilder // header 设置 .setHeaderParam("typ","JWT") .setHeaderParam("alg","HS256") // Payload 设置 .claim("username",username) .claim("role",roles) .setExpiration(new Date(System.currentTimeMillis()+EXPIRE_TIME)) // 设置 id .setId(String.valueOf(id)) // Signature 设置,主要是加密方式,和加盐参数 .signWith(SignatureAlgorithm.HS256,SALT) .compact(); return jwtPassword; } /** * 解析一个加密的密文,并将他封装成 Map 对象返回, * username:用户名, * roles:角色, * id:用户ID, * expireTime:过期时间, * head_typ:加密类型, * head_alg:加密方式, * @param jwtPassword 加密的密文 * * @return 封装了用户加密信息的 map 对象 */ public static HashMap jwtDecrypt(String jwtPassword){ HashMap<String, Object> decryMap = new HashMap<>(); // 传入加盐的参数,和加密对象,这里切记不能定义变量,一行写完 Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SALT).parseClaimsJws(jwtPassword); // 解析Body(Payload)部分的值 Claims body = claimsJws.getBody(); decryMap.put("username", body.get("username")); decryMap.put("roles", body.get("role")); decryMap.put("id", body.getId()); decryMap.put("expireTime", body.getExpiration()); // 解析头部的值 Header header = claimsJws.getHeader(); decryMap.put("head_typ",header.get("typ")); decryMap.put("head_alg",header.get("alg")); return decryMap; } }
如果自己还有工具包,可以往这个项目中放,后期好调用
,这里只是给出几个简单常用的工具包。
创建配置类接口
该项目主要是用于对Spring容器的一些配置,比如Redis的操作模板,Swagger配置,Mybatis-Plus的配置,等等,许多配置都可以放到这里面来,方便后期管理,主要是配置进入Spring容器的
-
创建项目Configuration-API
-
导入依赖,依赖根据自己在项目配置中需要什么依赖,就导入什么依赖,当前自需要配置以下三个,如果有更多配置,修改依赖即可
<dependencies> <dependency> <groupId>com.wyx</groupId> <artifactId>common-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--对Redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <!--Mybatis-Plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <!--引入swagger bootstrap ui--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> </dependency> </dependencies> <!--资源过滤器--> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources> </build>
-
Mybatis-Plus自动填充注解
package com.wyx.MybatisPlusConfig; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.util.Date; /** * Mybatis-Plus 中的字段填充配置类 */ @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 执行插入操作时,查找对应的字段名,和给它更新的值,并将元数据带入 this.setFieldValByName("createTime",new Date(),metaObject); this.setFieldValByName("updateTime",new Date(),metaObject); } @Override public void updateFill(MetaObject metaObject) { this.setFieldValByName("updateTime",new Date(),metaObject); } }
Mybatis-Plus乐观锁,分页插件配置类
package com.wyx.MybatisPlusConfig; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan("com.wyx.mapper,com.wyx.SpringSecurityConfig.mapper") //@MapperScan("com.wyx.mapper") 开始时是配置在主启动类上,当我们创建Mybatis-Plus的配置类时,一般情况下把它移动到我们的配置类上 public class MybatisPlusConfig { /** * 旧版 @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); } // 旧版 @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false // paginationInterceptor.setOverflow(false); // 设置最大单页限制数量,默认 500 条,-1 不受限制 // paginationInterceptor.setLimit(500); // 开启 count 的 join 优化,只针对部分 left join paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true)); return paginationInterceptor; } */ /** * 乐观锁插件,分页插件 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 乐观锁 mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 分页插件 mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2)); return mybatisPlusInterceptor; } }
-
Swagger配置类
package com.wyx.SwaggerConfig; import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI; import com.google.common.base.Predicates; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 @EnableSwaggerBootstrapUI public class Swagger2 { //默认文档地址(swagger-ui)为 http://localhost:端口号/swagger-ui.html //默认文档地址(bootstrap-ui)为 http://localhost:端口号/doc.html @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2) //指定Api类型为Swagger2 .apiInfo(apiInfo()) //指定文档汇总信息 .select() .apis(RequestHandlerSelectors .basePackage("com.wyx.controller")) //指定controller包路径 //.paths(PathSelectors.any()) //指定展示所有controller .paths(Predicates.not(PathSelectors.regex("/error.*"))) // 排除哪些路径不扫描 .build(); } private ApiInfo apiInfo(){ //返回一个apiinfo return new ApiInfoBuilder() .title("api接口文档") //文档页标题 .contact( new Contact( "王玉星", "https://www.cnblogs.com/Rampant/", "309597117@qq.com") ) // 联系人信息 .description("api文档") // 详细信息 .version("1.0.1") // 文档版本号 .termsOfServiceUrl("https://www.cnblogs.com/Rampant/") //网站地址 .build(); } }
-
Redis
模板和缓存配置package com.wyx.RedisConfig; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @EnableCaching //开启缓存 @Configuration //配置类 public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setConnectionFactory(factory); //key序列化方式 template.setKeySerializer(redisSerializer); //value序列化 template.setValueSerializer(jackson2JsonRedisSerializer); //value hashmap序列化 template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题),过期时间600秒 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } }
-
异常处理类
package com.wyx.exceptionhandler; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor //生成有参数构造方法 @NoArgsConstructor //生成无参数构造 public class MyException extends RuntimeException { private Integer code;//状态码 private String msg;//异常信息 }
package com.wyx.exceptionhandler; import com.wyx.utils.CommonResult; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice @Slf4j public class GlobalExceptionHandler { //指定出现什么异常执行这个方法 @ExceptionHandler(Exception.class) @ResponseBody //为了返回数据 public CommonResult error(Exception e) { log.error("处理异常"); e.printStackTrace(); return CommonResult.Error(); } //特定异常 @ExceptionHandler(ArithmeticException.class) @ResponseBody //为了返回数据 public CommonResult error(ArithmeticException e) { log.error("算术处理异常"); e.printStackTrace(); return CommonResult.Error(); } //自定义异常 @ExceptionHandler(MyException.class) @ResponseBody //为了返回数据 public CommonResult error(MyException e) { log.error(e.getMessage()); e.printStackTrace(); return CommonResult.Error(); } }
-
SpringBoot自定义错误返回
package com.wyx.springbootconfig; import com.wyx.utils.CommonResult; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** * @ClassName MyErrorController * @Description TODO * @Author 王玉星 * @Date 2021/8/8 23:42 * @Version 1.0 */ @RestController public class MyErrorController implements ErrorController { @RequestMapping("/error") public CommonResult handleError(HttpServletRequest request){ //获取statusCode:401,404,500 Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); if(statusCode == 401){ return CommonResult.perNoAll(); }else if(statusCode == 404){ return CommonResult.isNull(); }else if(statusCode == 403){ return CommonResult.ErrorMethod(); }else{ return CommonResult.MyMessage("出现了未知异常,请联系管理员"); } } @Override public String getErrorPath() { return "/error"; } }
SpringSecurity安全配置(重点)
-
自定义SpringSecurity加密配置,如果时用户注册时请调用
encodeRandom
方法,随机生成加密密文,和加密盐package com.wyx.SpringSecurityConfig; import com.wyx.utils.PasswordEncryption; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Random; /** * 自定义密码加密功能 * */ @Component public class MyPasswordEncoder implements PasswordEncoder { private String salt; @Autowired HttpServletRequest request; public MyPasswordEncoder() { this(-1); } public MyPasswordEncoder(int strength) { } /** * 用于用户注册时使用 * 对密码使用随机的密码经行加密,返回加密的值,和加密盐,返回形式如下:“加密值,加密的随机盐” * @param rawPassword 传入密码 * * @return 返回加密的密码和随机生成的盐,两者通过,分开。 */ public String encodeRandom(CharSequence rawPassword) { String randomSalt = setRandomSalt(); return PasswordEncryption.encryption((String) rawPassword,randomSalt)+","+randomSalt; } /** * 对密码经行加密的函数 * @param rawPassword 输入的密码 * * @return 加密后返回的密码 */ @Override public String encode(CharSequence rawPassword) { setSalt(); return PasswordEncryption.encryption((String) rawPassword,getSalt()); } /** * 对输入的密码与数据库密码经行对比 * @param rawPassword 输入的密码 * @param encodedPassword 数据库中查出来的密码 * * @return 返回对比结果true false */ @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals((String.valueOf(rawPassword))); } /** * 放回随机加密的盐 * @return String 随机加密的盐 */ public String setRandomSalt(){ String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random=new Random(); StringBuffer sb=new StringBuffer(); for(int i=0;i<6;i++){ int number=random.nextInt(62); sb.append(str.charAt(number)); } return sb.toString(); } public String getSalt() { return salt; } public void setSalt() { this.salt = (String) request.getAttribute("loginSalt"); } }
-
权限不通过的处理类
package com.wyx.SpringSecurityConfig; import lombok.extern.java.Log; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Log public class UnauthEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletRequest.getRequestDispatcher("/authority/noAuthority").forward(httpServletRequest,httpServletResponse); httpServletRequest.getServletPath(); log.warning("请求路径:"+httpServletRequest.getServletPath()+",权限不允许"); } }
-
登录授权用户实体类
package com.wyx.SpringSecurityConfig.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.io.Serializable; /** * @ClassName SecurityUserMapper * @Description TODO * @Author 王玉星 * @Date 2021/8/6 17:33 * @Version 1.0 */ @Data @AllArgsConstructor @NoArgsConstructor @EqualsAndHashCode(callSuper = false) @TableName("jwt_user") @ApiModel(value="User对象", description="用户表") public class SecurityUser implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "用户id") @TableId(value = "id", type = IdType.ASSIGN_ID) private String id; @ApiModelProperty(value = "用户名") private String username; @ApiModelProperty(value = "用户密码") private String password; @ApiModelProperty(value = "密码加密盐") private String salt; }
-
登录查询接口
package com.wyx.SpringSecurityConfig.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.wyx.SpringSecurityConfig.pojo.SecurityUser; import java.util.List; /** * <p> * 登录用户 Mapper 接口 * </p> * * @author 王玉星 * @since 2021-08-03 */ public interface SecurityUserMapper extends BaseMapper<SecurityUserMapper> { SecurityUser getSecurityUserByUserName(String username); List<String> getRoleListByUserName(String username); }
-
自定义登录查询
Mapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper"> <resultMap id="securityUser" type="com.wyx.SpringSecurityConfig.pojo.SecurityUser"> <result column="id" property="id"/> <result column="password" property="password"/> <result column="salt" property="salt"/> <result column="username" property="username"/> </resultMap> <select id="getSecurityUserByUserName" resultMap="securityUser" parameterType="string"> select id,username,salt,password from jwt_user where username = #{username} </select> <select id="getRoleListByUserName" resultType="java.lang.String"> select role_code from jwt_role where id in (select role_id from jwt_user_role where user_id in (select id from jwt_user where username = #{username}) ) </select> </mapper>
-
授权处理器
package com.wyx.SpringSecurityConfig; import com.wyx.exceptionhandler.MyException; import com.wyx.utils.CommonResult; import com.wyx.utils.TokenManager; import lombok.extern.java.Log; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class TokenAuthFilter extends BasicAuthenticationFilter { private RedisTemplate redisTemplate; private AuthenticationManager authenticationManager; public TokenAuthFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) { super(authenticationManager); this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //获取当前认证成功用户权限信息 UsernamePasswordAuthenticationToken authRequest = getAuthentication(request); //判断如果有权限信息,放到权限上下文中 if(authRequest != null) { SecurityContextHolder.getContext().setAuthentication(authRequest); } chain.doFilter(request,response); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { //从header获取token String token = request.getHeader("token"); if(token != null) { HashMap tokenMap = null; try { tokenMap = TokenManager.jwtDecrypt(token); }catch (RuntimeException e){ logger.error("令牌已过期"); return null; } String username = (String) tokenMap.get("username"); if (!redisTemplate.opsForValue().get(username).equals(token)){ System.out.println(token); System.out.println(redisTemplate.opsForValue().get(username)); logger.error("令牌错误"); return null; } ArrayList roles = (ArrayList) tokenMap.get("roles"); StringBuilder authRoles = new StringBuilder(); for (Object role : roles) { role = "ROLE_"+role.toString(); authRoles.append(role); authRoles.append(","); } // 消除最后一个分号 authRoles.replace(authRoles.length()-1,authRoles.length(),""); /* //从redis获取对应权限列表 List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username); Collection<GrantedAuthority> authority = new ArrayList<>(); for(String permissionValue : permissionValueList) { SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue); authority.add(auth); }*/ List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(String.valueOf(authRoles)); // return new UsernamePasswordAuthenticationToken(tokenMap.get("username"),token,authority); // 已经通过令牌认证成功,密码设置为空即可 User user = new User(username,"",authorityList); return new UsernamePasswordAuthenticationToken(user,token,authorityList); } return null; } }
-
登录过滤器
package com.wyx.SpringSecurityConfig; import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper; import com.wyx.SpringSecurityConfig.pojo.SecurityUser; import com.wyx.utils.PasswordEncryption; import com.wyx.utils.TokenManager; import lombok.extern.java.Log; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Random; import java.util.concurrent.TimeUnit; @Log public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private final RedisTemplate redisTemplate; private final SecurityUserMapper securityUserMapper; private final AuthenticationManager authenticationManager; public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate, SecurityUserMapper securityUserMapper) { this.redisTemplate = redisTemplate; this.authenticationManager = authenticationManager; this.securityUserMapper = securityUserMapper; this.setPostOnly(false); this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST")); } //1 获取表单提交用户名和密码 @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //获取表单提交数据 //使用流方式获取表单信息 //Users user = new ObjectMapper().readValue(request.getInputStream(), Users.class); //User users = new User(request.getParameter("username"), request.getParameter("password")); // 设置盐,从数据库中获取 SecurityUser user = securityUserMapper.getSecurityUserByUserName(request.getParameter("username")); // 将密码修改为用户输入的 user.setPassword(request.getParameter("password")); // 将加密的盐保存到请求中 request.setAttribute("loginSalt",user.getSalt()); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), PasswordEncryption.encryption(user.getPassword(),user.getSalt()), new ArrayList<>())); } //2 认证成功调用的方法 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) { //认证成功,得到认证成功之后用户信息 User user = (User) authResult.getPrincipal(); //查询数据库获取用户ID 用户名,用户权限。 String username = user.getUsername(); StringBuilder roles = new StringBuilder(); for (GrantedAuthority authority : authResult.getAuthorities()) { roles.append(authority); roles.append(","); } // 消除最后一个分号 if (roles.length()>=1){ roles.replace(roles.length()-1,roles.length(),""); } String token = TokenManager.encryption(Long.valueOf(new Random().nextInt()),username, String.valueOf(roles)); //把用户名称和用户权限列表放到redis,并设置过期时间 HashMap tokenMap = TokenManager.jwtDecrypt(token); Date date = (Date) tokenMap.get("expireTime"); redisTemplate.opsForValue().set(username,token,(date.getTime()-new Date().getTime())/1000, TimeUnit.SECONDS); //返回token response.setHeader("token",token); } //3 认证失败调用的方法 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws ServletException, IOException { request.getRequestDispatcher("/authority/AuthenticationFailed").forward(request,response); log.warning("认证失败,请重新认证"); } }
-
退出登录处理器
package com.wyx.SpringSecurityConfig; import com.wyx.utils.TokenManager; import lombok.extern.java.Log; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; //退出处理器 @Log public class TokenLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { // 功能较为简单,只要请求头包含token就能退出,后期根据自己的业务需求更改 String token = request.getHeader("token"); if(token != null) { // 从 token中获取用户名 HashMap tokenMap = TokenManager.jwtDecrypt(token); try { response.sendRedirect("/authority/logoutOk"); } catch (IOException e) { e.printStackTrace(); } log.info("用户退出登录成功"); // 在 redis 上删除token,可以删除,也可以不删。 // redisTemplate.delete(tokenMap.get("username")); }else { try { request.getRequestDispatcher("/authority/logoutEr").forward(request,response); } catch (ServletException | IOException e) { e.printStackTrace(); } } } }
-
获取数据库用户信息处理器
package com.wyx.SpringSecurityConfig; import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper; import com.wyx.SpringSecurityConfig.pojo.SecurityUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; @Service("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { @Resource SecurityUserMapper securityUserMapper; @Autowired MyPasswordEncoder myPasswordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SecurityUser user = securityUserMapper.getSecurityUserByUserName(username); List<String> roleListByUserName = securityUserMapper.getRoleListByUserName(username); // 设置权限,通过逗号分割 StringBuilder role = new StringBuilder(); for (String s : roleListByUserName) { role.append("ROLE_"+s); role.append(","); } //消除最后一个分号 if (role.length()>=1){ role.replace(role.length()-1,role.length(),""); }else { role.append(""); } List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(String.valueOf(role)); SecurityUser securityUser = new SecurityUser(user.getId(),user.getUsername(), user.getPassword(),user.getSalt()); if (securityUser.getUsername()==null){ throw new UsernameNotFoundException("用户名不存在!"); } return new User(securityUser.getUsername(),securityUser.getPassword(),authorityList); } }
-
安全框架总配置
package com.wyx.SpringSecurityConfig; import com.wyx.SpringSecurityConfig.mapper.SecurityUserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import javax.annotation.Resource; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private RedisTemplate redisTemplate; @Autowired private MyPasswordEncoder myPasswordEncoder; @Autowired private UserDetailsService userDetailsService; @Resource SecurityUserMapper securityUserMapper; /** * 配置设置 * @param http * @throws Exception */ //设置退出的地址和token,redis操作地址 @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问处理器 .and().csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and().logout().logoutUrl("/logout").permitAll()//退出路径 .addLogoutHandler(new TokenLogoutHandler()).and() //退出登录处理器 .addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate, securityUserMapper)) //登录处理器 .addFilter(new TokenAuthFilter(authenticationManager(),redisTemplate)).httpBasic(); //授权处理器 } //调用userDetailsService和密码处理 @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(myPasswordEncoder); } //不进行认证的路径,可以直接访问 @Override public void configure(WebSecurity web) throws Exception { // 配置过了swagger ui 的过滤器 web.ignoring().antMatchers( "/error", "/error/**", "/authority/logoutOk", "/doc.html","/webjars/**", "/v2/api-docs", "/swagger-resources", "/swagger-resources/**", "/configuration/ui", "/configuration/security", "/swagger-ui.html/**", "/csrf","/" ); } }
-
权限信息返回控制器
package com.wyx.SpringSecurityConfig.controller; import com.wyx.utils.CommonResult; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; /** * @ClassName AuthorityController * @Description TODO * @Author 王玉星 * @Date 2021/8/7 18:10 * @Version 1.0 */ @RestController @RequestMapping("/authority") public class AuthorityController { // 无权限访问响应 @RequestMapping("/noAuthority") public CommonResult noAuthority(){ return CommonResult.perNoAll(); } // 认证失败响应 @RequestMapping("/AuthenticationFailed") public CommonResult AuthenticationFailed(){ return CommonResult.MyMessage("认证失败,用户名或密码错误"); } // 退出成功处理器 @RequestMapping("/logoutOk") public CommonResult logoutOk(HttpServletResponse response){ // 给前端一个空的请求头,让前端将请求头参数设置为空即可 response.setHeader("token",""); return CommonResult.Success("退出登录成功😀😀😀"); } // 退出失败处理器 @RequestMapping("/logoutEr") public CommonResult logoutEr(){ return CommonResult.MyMessage("退出登录失败 😢😢😢"); } }
-
跨域请求伪造处理
package com.wyx.csrfconfig; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.util.pattern.PathPatternParser; @Configuration public class CorsConfig { //解决跨域 @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**",config); return new CorsWebFilter(source); } }
服务提供者搭建
-
创建新模块 Service-Provider-Main-9110 Service-Provider-Main-9111,两者搭建都一样
-
导入依赖
<dependencies> <dependency> <groupId>com.wyx</groupId> <artifactId>common-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>com.wyx</groupId> <artifactId>Configuration-API</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> </dependency> <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies>
-
application.yaml
server: port: 9110 spring: application: name: Service-Provider datasource: url: jdbc:mysql://47.97.218.81:3306/Security_JWT?useSSL=true&useUnicode=true&charterEncoding=utf8&serverTimezone=GMT%2B8 password: 970699 username: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: DateHikariCP # 连接池名 minimum-idle: 5 # 最小空闲连接数 idle-timeout: 18000 # 空闲连接最大存活时间 单位毫秒 maximum-pool-size: 10 # 最大连接数 auto-commit: true # 是否自动提交(返回连接池时) max-lifetime: 18000 # 连接最大存活时间 单位毫秒 connection-timeout: 3000 # 连接超时时间 单位毫秒 #connection-test-query: SELECT 1 # 测试连接是否可以用的查询语句 cloud: nacos: discovery: server-addr: 47.97.218.81:8848 # 连接的注册中心 redis: host: 47.97.218.81 # redis 地址 port: 6379 # redis 端口号 database: 0 # 使用数据库 password: 970699 # 连接密码 timeout: 1800000 #超时时间(毫秒) lettuce: pool: #连接池 min-idle: 0 # 最小连接 max-idle: 5 #最大连接 max-active: 20 # 连接存活时间 max-wait: -1 # 最大等待时间 jackson: date-format: yyyy-MM-dd HH:mm:ss #返回的json 时间日期格式 time-zone: GMT+8 # 设置时区 mybatis-plus: mapper-locations: classpath:mapper/*.xml #mybatis-plus xml的地址 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #配置mybatis-plus的日志输出 management: endpoints: web: exposure: include: '*' #暴露端口
-
主启动类
package com.wyx; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @ClassName com.wyx.ServiceProviderMain9110 * @Description TODO * @Author 王玉星 * @Date 2021/8/3 15:11 * @Version 1.0 */ @SpringBootApplication @EnableDiscoveryClient public class ServiceProviderMain9110 { public static void main(String[] args) { SpringApplication.run(ServiceProviderMain9110.class,args); } }
使用Mabatis-plus代码生成器,生成项目代码
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.Arrays;
/**
* @ClassName CodeGeneratorMain
* @Description TODO
* @Author 王玉星
* @Date 2021/7/31 12:07
* @Version 1.0
*/
public class CodeGeneratorMain {
public static void main(String[] args) {
//配置代码生成器对象
AutoGenerator generator = new AutoGenerator();
// 配置策略
// 1、全局配置
GlobalConfig gc = new GlobalConfig();
//获取当前项目路径
String projectPath = System.getProperty("user.dir");
//生成代码路径 等于当前项目路径+模块路径+/src/main/java
gc.setOutputDir(projectPath + "/Service-Provider-Main-9110/src/main/java");
gc.setAuthor("王玉星"); //作者信息
gc.setOpen(false); //是否打开资源管理器
gc.setFileOverride(false); //生成文件是否覆盖
gc.setSwagger2(true); //实体属性 Swagger2 注解
//去掉Services实现类的I前缀,并在名字后面加上Impl
gc.setServiceImplName("%sServiceImpl");
//去掉Service接口的I前缀
gc.setServiceName("%sService");
gc.setIdType(IdType.ASSIGN_ID); //主键生成策略
gc.setDateType(DateType.ONLY_DATE); //设置时间类型
//将全局配置放入代码生成器对象中
generator.setGlobalConfig(gc);
// 2、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
//连接url
dsc.setUrl("jdbc:mysql://47.97.218.81:3306/Security_JWT?useSSL=true&useUnicode=true&charterEncoding=utf8&serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver"); //连接驱动
dsc.setUsername("root"); //连接名称
dsc.setPassword("970699"); //连接密码
dsc.setDbType(DbType.MYSQL); //连接数据库类型
//将数据源配置放入代码生成器对象中
generator.setDataSource(dsc);
// 3、包配置
PackageConfig pc = new PackageConfig();
// pc.setModuleName("CodeGenerator"); //生成的模块
pc.setParent(""); //生成的父路径设置为空
String classParentPath = "com.wyx."; // 设置类的父路径
String fileParentPath = ""; // 设置配置文件的父路径
pc.setEntity(classParentPath+"pojo"); //实体类包
pc.setService(classParentPath+ "service"); //service层包
pc.setController(classParentPath+"controller"); //controller 层包
pc.setMapper(classParentPath+ "mapper"); //mapper 层包
pc.setServiceImpl(classParentPath+"service.impl"); //ServiceImpl层包
pc.setXml(fileParentPath+ "mapper");
//将生成包规则放入代码生成器对象中
generator.setPackageInfo(pc);
// 4、配置策略
StrategyConfig strategy = new StrategyConfig();
//设置要映射的表,看数据库的表来(最重要),多张数据库表逗号来传递多个表名
strategy.setInclude("jwt_user","jwt_role","jwt_user_role");
strategy.setTablePrefix("jwt_"); // 移除表前缀
// strategy.setFieldPrefix("role_"); // 移除字段前缀
strategy.setNaming(NamingStrategy.underline_to_camel); //设置命名规则 下划线转驼峰命名
strategy.setColumnNaming(NamingStrategy.underline_to_camel); //设置命名规则
strategy.setEntityLombokModel(true); //自动使用Lombok
//逻辑删除策略 的字段名
strategy.setLogicDeleteFieldName("deleted");
// 自动填充配置
TableFill createTime = new TableFill("create_time", FieldFill.INSERT); //自动填充策略配置
TableFill updateTime = new TableFill("update_time", FieldFill.INSERT_UPDATE); //自动更新策略配置
strategy.setTableFillList(Arrays.asList(createTime,updateTime));
//乐观锁
strategy.setVersionFieldName("version"); //设置乐观锁
//生成 @RestController 控制器 @Controller -> @RestController
strategy.setRestControllerStyle(true);
//驼峰转连字符 @RequestMapping("/managerUserActionHistory") -> @RequestMapping("/manager-user-action-history")
strategy.setControllerMappingHyphenStyle(true);
//将配置的策略放入代码生成器对象中
generator.setStrategy(strategy);
generator.execute(); //执行代码生成器方法
}
}
运行生成完成后,在/src/main/java/
下有一个mapper
的文件夹,将其移动到resources
目录下,让其和配置文件application
中的配置路径一样。
移动成功后,可以得到如下的目录结构
业务类编写
情况说明,这里主要是项目搭建,所以业务类,随便写一个接口即可,不多写
package com.wyx.controller;
import com.wyx.pojo.User;
import com.wyx.service.UserService;
import com.wyx.utils.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 用户表 前端控制器
* </p>
*
* @author 王玉星
* @since 2021-08-03
*/
@RequiredArgsConstructor
@Data
@RestController
@RequestMapping("/user")
@Api(tags = "用户信息接口", value = "对用户信息的增删改查,等一系列的控制") // 标注这个类的主要作用,tags 在 ui 显示,value 不显示
public class UserController {
@NonNull
UserService userService;
// value 表示方法的作用在 ui 上显示 ,notes 方法的具体描述 , 给方法重新打开一个接口文档
@ApiOperation(value = "根据id获取用户信息", notes = " \n 用户请求 /get/1 对应获取用户ID 为 1 的用户信息", tags = "查询单个用户信息")
// 表示参数的信息,name 参数名,value 参数说明(ui显示),paramType 请求参数类型(path 路径参数,query 查询参数)
// dataType 请求参数的类型,required 是否为必须,默认false
@ApiImplicitParam(name = "id", value = "用户ID", paramType = "path" ,dataType = "String",required = true)
/*
多参数时使用如下
@ApiImplicitParams(
@ApiImplicitParam(),
@ApiImplicitParam
)
*/
@GetMapping("/get/{id}")
public CommonResult getId(@PathVariable String id){
User user = userService.getById(id);
return (user == null) ? CommonResult.isNull():CommonResult.Success(user);
}
/*
@RequiredArgsConstructor,来源于 lombok
解决我们在注入参数是需要使用
@Autowired
private UserService userService;
我们只需要在类上添加 @RequiredArgsConstructor,在注入是写出如下 需要注意的是在注入时需要用final定义,或者使用@notnull注解
final UserService userService;
或者
@NonNull
UserService userService;
@RequiredArgsConstructor 的几个常用参数
1. access 声明注入的对象的属性 public private protected(默认 public)
eg : @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
2. staticName 将类中的静态方法注明,其实在实战中,使用较少
eg : @RequiredArgsConstructor(staticName = "getId") (默认为空,getId方法名)
3. onConstructor 设置构造器注入时使用的注解
在 jdk1.7之前如下 代表注入构造器注入时,使用 @Autowired注入
eg :onConstructor=@__({@Autowired})
在 jdk1.8之后如下
eg :onConstructor_={@Autowired}
*/
}
启动测试访问:
可以访问如下地址查看自己的相关信息
bootstrap-ui 地址:http://localhost:9110/doc.html
swagger-ui 地址:http://localhost:9110/swagger-ui.html
微服务网关搭建
-
创建项目 SpringCloud-Provider-Gateway-9527
-
导入 pom.xml
<dependencies> <!--Spring-gateway 启动依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--spring-boot-web 模块 常用的3个 网关不需要,否则启动不了--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-starter-web</artifactId>--> <!-- </dependency>--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--热部署插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!--测试插件--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
修改 yaml
server: port: 9527 spring: application: name: cloud-gateway cloud: nacos: discovery: server-addr: 47.97.218.81:8848 # 连接的注册中心 gateway: discovery: locator: enabled: true #开启网关拉取nacos的服务 routes: - id: routh #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: lb://Service-Provider #匹配后提供服务的路由地址 predicates: - Path=/gateway/api/** #断言,路径相匹配的进行路由 filters: - StripPrefix=2 # 剔除前缀的几个参数 - AddResponseHeader=X-Response-Default-Foo, Default-Bar # 添加响应头
-
主启动类
package com.wyx; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * @ClassName SpringCloudProviderGateway9527 * @Description TODO * @Author 王玉星 * @Date 2021/8/8 15:01 * @Version 1.0 */ @EnableDiscoveryClient @SpringBootApplication public class SpringCloudProviderGateway9527 { public static void main(String[] args) { SpringApplication.run(SpringCloudProviderGateway9527.class,args); } }
启动网关,访问原来的路径前面加上/gateway/api/
这里访问 bootstrap-ui地址为:http://localhost:9527/gateway/api/doc.html
到此:基于微服务的单点登录
授权,管理完成具体的业务逻辑修改即可
该项目已同步到码云
:需要可以下载https://gitee.com/Rampants/SpringSecurity-JWT
【推荐】博客园携手 AI 驱动开发工具商 Chat2DB 推出联合终身会员
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux实时系统Xenomai宕机问题的深度定位过程
· 记一次 .NET某汗液测试机系统 崩溃分析
· 深度解析Mamba与状态空间模型:一图带你轻松入门
· 记一次 .NET某电商医药网站 CPU爆高分析
· 内存条的基本知识与选购指南
· 2024年终总结 : 迷茫, 尝试突破, 内耗, 释怀
· 开源商业化 Sealos 如何做到月入 160万
· 《花100块做个摸鱼小网站! 》番外篇—小网站竟然让我赚到钱了
· 2025你好
· Coravel:一个可轻松实现任务调度、队列、邮件发送的开源项目