├── .gitignore
├── LICENSE
├── README.md
├── pom.xml
└── src
└── main
├── java
└── com
│ └── wang
│ ├── Application.java
│ ├── config
│ ├── ExceptionAdvice.java
│ ├── OriginFilter.java
│ ├── redis
│ │ └── JedisConfig.java
│ └── shiro
│ │ ├── ShiroConfig.java
│ │ ├── UserRealm.java
│ │ ├── cache
│ │ ├── CustomCache.java
│ │ └── CustomCacheManager.java
│ │ └── jwt
│ │ ├── JwtFilter.java
│ │ └── JwtToken.java
│ ├── controller
│ └── UserController.java
│ ├── exception
│ ├── CustomException.java
│ └── CustomUnauthorizedException.java
│ ├── mapper
│ ├── PermissionMapper.java
│ ├── RoleMapper.java
│ ├── RolePermissionMapper.java
│ ├── UserMapper.java
│ └── UserRoleMapper.java
│ ├── model
│ ├── PermissionDto.java
│ ├── RoleDto.java
│ ├── RolePermissionDto.java
│ ├── UserDto.java
│ ├── UserRoleDto.java
│ ├── common
│ │ ├── BaseDto.java
│ │ ├── Constant.java
│ │ └── ResponseBean.java
│ ├── entity
│ │ ├── Permission.java
│ │ ├── Role.java
│ │ ├── RolePermission.java
│ │ ├── User.java
│ │ └── UserRole.java
│ └── valid
│ │ └── group
│ │ ├── UserEditValidGroup.java
│ │ └── UserLoginValidGroup.java
│ ├── service
│ ├── IBaseService.java
│ ├── IUserService.java
│ └── impl
│ │ ├── BaseServiceImpl.java
│ │ └── UserServiceImpl.java
│ └── util
│ ├── AesCipherUtil.java
│ ├── JedisUtil.java
│ ├── JwtUtil.java
│ ├── UserUtil.java
│ └── common
│ ├── Base64ConvertUtil.java
│ ├── HexConvertUtil.java
│ ├── JsonConvertUtil.java
│ ├── PropertiesUtil.java
│ ├── SerializableUtil.java
│ └── StringUtil.java
└── resources
├── application.yml
├── config.properties
├── generator
└── generatorConfig.xml
├── mapper
├── PermissionMapper.xml
├── RoleMapper.xml
├── RolePermissionMapper.xml
├── UserMapper.xml
└── UserRoleMapper.xml
└── sql
└── MySQL.sql
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Example user template template
3 | ### Example user template
4 |
5 | # IntelliJ project files
6 | .idea
7 | *.iml
8 | out
9 | gen
10 | target
11 | src/test
12 | *.iws
13 | *.ipr
14 |
15 | gen### Java template
16 | # Compiled class file
17 | *.class
18 |
19 | # Log file
20 | *.log
21 |
22 | # BlueJ files
23 | *.ctxt
24 |
25 | # Mobile Tools for Java (J2ME)
26 | .mtj.tmp/
27 |
28 | # Package Files #
29 | *.jar
30 | *.war
31 | *.nar
32 | *.ear
33 | *.zip
34 | *.tar.gz
35 | *.rar
36 |
37 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
38 | hs_err_pid*
39 |
40 | ### STS ###
41 | .apt_generated
42 | .classpath
43 | .factorypath
44 | .project
45 | .settings
46 | .springBeans
47 | .sts4-cache
48 |
49 | ### NetBeans ###
50 | /nbproject/private/
51 | /build/
52 | /nbbuild/
53 | /dist/
54 | /nbdist/
55 | /.nb-gradle/
56 |
57 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 随心
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## ShiroJwt
2 |
3 | [](LICENSE)
4 | [](https://github.com/wang926454/ShiroJwt/pulls)
5 | [](https://github.com/wang926454/ShiroJwt)
6 | [](https://github.com/wang926454/ShiroJwt)
7 |
8 | > 前端地址:[https://github.com/wang926454/VueStudy/tree/master/VueStudy08-JWT](https://github.com/wang926454/VueStudy/tree/master/VueStudy08-JWT)
9 |
10 | #### 疑问查看
11 |
12 | 1. [#14 重复请求会不会生成多个token](https://github.com/dolyw/ShiroJwt/issues/14)
13 | 2. [#19 跨域sso问题](https://github.com/dolyw/ShiroJwt/issues/19)
14 | 3. [#29 Token刷新并发处理](https://github.com/dolyw/ShiroJwt/issues/29)
15 |
16 |
17 |
18 | 有疑问请扫码加**QQ**群交流: **779168604**
19 |
20 | #### 项目相关
21 |
22 | * JavaDoc:[https://apidoc.gitee.com/dolyw/ShiroJwt](https://apidoc.gitee.com/dolyw/ShiroJwt)
23 | * 接口文档:[https://note.dolyw.com/shirojwt/ShiroJwt-Interface.html](https://note.dolyw.com/shirojwt/ShiroJwt-Interface.html)
24 | * 教程目录:[https://note.dolyw.com/shirojwt](https://note.dolyw.com/shirojwt)
25 | * 改为数据库形式(MySQL):[https://note.dolyw.com/shirojwt/ShiroJwt02-MySQL.html](https://note.dolyw.com/shirojwt/ShiroJwt02-MySQL.html)
26 | * 解决无法直接返回401错误:[https://note.dolyw.com/shirojwt/ShiroJwt03-401.html](https://note.dolyw.com/shirojwt/ShiroJwt03-401.html)
27 | * 实现Shiro的Cache(Redis)功能:[https://note.dolyw.com/shirojwt/ShiroJwt04-Redis.html](https://note.dolyw.com/shirojwt/ShiroJwt04-Redis.html)
28 |
29 | #### 项目介绍
30 |
31 | 1. RESTful API
32 | 2. Maven集成Mybatis Generator(逆向工程)
33 | 3. Shiro + Java-JWT实现无状态鉴权机制(Token)
34 | 4. 密码加密(采用AES-128 + Base64的方式)
35 | 5. 集成Redis(Jedis)
36 | 6. 重写Shiro缓存机制(Redis)
37 | 7. Redis中保存RefreshToken信息(做到JWT的可控性)
38 | 8. 根据RefreshToken自动刷新AccessToken
39 |
40 | ##### 关于Shiro + Java-JWT实现无状态鉴权机制(Token)
41 |
42 | > 1. 首先**Post**用户名与密码到**user/login**登入,成功返回加密的**AccessToken**,失败直接返回401错误(帐号或密码不正确)
43 | > 2. 以后访问都带上这个**AccessToken**即可
44 | > 3. 鉴权流程主要是重写了**Shiro**的入口过滤器**JWTFilter**(**BasicHttpAuthenticationFilter**),判断请求**Header**里面是否包含**Authorization**字段
45 | > 4. 有就进行**Shiro**的**Token**登录认证授权(用户访问每一个需要权限的请求必须在**Header**中添加**Authorization**字段存放**AccessToken**),没有就以游客直接访问(有权限管控的话,以游客访问就会被拦截)
46 |
47 | ##### 关于AES-128 + Base64当两个用户的明文密码相同时进行加密,会发现数据库中存在相同结构的暗文密码
48 |
49 | > 大部分是以**MD5 + 盐**的形式解决了这个问题(详细自己百度),我采用**AES-128 + Base64**是以帐号+密码的形式进行加密密码,因为帐号具有唯一性,所以也不会出现相同结构的暗文密码这个问题
50 |
51 | ##### 关于将Jedis工具类与SpringBoot整合
52 |
53 | > 本来是直接将**JedisUtil**注入为**Bean**,每次使用直接`@Autowired`注入使用即可,但是在重写**Shiro**的**CustomCache**无法注入**JedisUtil**,所以就改成静态注入**JedisPool连接池**,**JedisUtil工具类**还是直接调用静态方法,无需`@Autowired`注入
54 |
55 | ##### 关于Redis中保存RefreshToken信息(做到JWT的可控性)
56 |
57 | > 1. 登录认证通过后返回**AccessToken**信息(在**AccessToken**中**保存当前的时间戳和帐号**)
58 | > 2. 同时在**Redis**中设置一条以**帐号为Key,Value为当前时间戳(登录时间)**的**RefreshToken**
59 | > 3. 现在认证时必须**AccessToken**没失效以及**Redis**存在所对应的**RefreshToken**,且**RefreshToken时间戳**和**AccessToken信息中时间戳一致**才算认证通过,这样可以做到**JWT的可控性**
60 | > 4. 如果重新登录获取了新的**AccessToken**,旧的**AccessToken**就认证不了,因为**Redis**中所存放的的**RefreshToken时间戳信息**只会和最新生成的**AccessToken信息中携带的时间戳一致**,这样每个用户就只能使用最新的**AccessToken**认证
61 | > 5. **Redis**的**RefreshToken**也可以用来判断用户是否在线,如果删除**Redis**的某个**RefreshToken**,那这个**RefreshToken**所对应的**AccessToken**之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户
62 |
63 | ##### 关于根据RefreshToken自动刷新AccessToken
64 |
65 | > 1. 本身**AccessToken的过期时间为5分钟**(配置文件可配置),**RefreshToken过期时间为30分钟**(配置文件可配置)
66 | > 2. 当登录后时间过了5分钟之后,当前**AccessToken**便会过期失效,再次带上**AccessToken**访问**JWT**会抛出**TokenExpiredException**异常说明**Token**过期
67 | > 3. 开始判断是否要**进行AccessToken刷新**,**Redis查询当前用户的RefreshToken是否存在**,**以及这个RefreshToken所携带时间戳**和**过期AccessToken所携带的时间戳**是否**一致**
68 | > 4. **如果存在且一致就进行AccessToken刷新,设置过期时间为5分钟(配置文件可配置),时间戳为当前最新时间戳,同时也设置RefreshToken中的时间戳为当前最新时间戳,刷新过期时间重新为30分钟过期(配置文件可配置)**
69 | > 5. 最终将刷新的**AccessToken**存放在**Response的Header中的Authorization字段**返回(前端进行获取替换,下次用新的**AccessToken**进行访问)
70 |
71 | #### 软件架构
72 |
73 | 1. SpringBoot + Mybatis核心框架
74 | 2. PageHelper插件 + 通用Mapper插件
75 | 3. Shiro + Java-JWT无状态鉴权认证机制
76 | 4. Redis(Jedis)缓存框架
77 |
78 | #### 安装教程
79 |
80 | 1. 数据库帐号密码默认为root,如有修改,请自行修改配置文件application.yml
81 | 2. 解压后执行src\main\resources\sql\MySQL.sql脚本创建数据库和表
82 | 3. Redis需要自行安装Redis服务,端口密码默认
83 | 4. SpringBoot直接启动即可,测试工具PostMan
84 |
85 | #### 使用说明
86 |
87 | ##### Mybatis Generator使用(可视化自定义模板快速生成基础代码:[https://github.com/wang926454/ViewGenerator](https://github.com/wang926454/ViewGenerator))
88 |
89 | 先配置src\main\resources\generator\generatorConfig.xml文件(默认配置都在原来包的下一级reverse包下),在pom.xml这一级目录(即项目根目录下)的命令行窗口执行(前提是配置了mvn)(IDEA可以直接在Maven窗口Plugins中双击执行)
90 | ```shell
91 | mvn mybatis-generator:generate
92 | ```
93 |
94 | ##### PostMan使用(Token获取及使用)
95 |
96 | ```java
97 | 先设置Content-Type为application/json
98 | ```
99 | 
100 | ```text
101 | 然后填写请求参数帐号密码信息
102 | ```
103 | 
104 | ```text
105 | 进行请求访问,请求访问成功
106 | ```
107 | 
108 | ```java
109 | 点击查看Header信息的Authorization属性即是Token字段
110 | ```
111 | 
112 | ```java
113 | 访问需要权限的请求将Token字段放在Header信息的Authorization属性访问即可
114 | ```
115 | 
116 | ```java
117 | Token的自动刷新也是在Token失效时返回新的Token在Header信息的Authorization属性
118 | ```
119 |
120 | #### 搭建参考
121 |
122 | 1. 感谢SmithCruise的Shiro+JWT+Spring Boot Restful简易教程:[https://www.jianshu.com/p/f37f8c295057](https://www.jianshu.com/p/f37f8c295057)
123 | 2. 感谢王洪玉的[Shiro入门](一)使用Redis作为缓存管理器:[https://blog.csdn.net/why15732625998/article/details/78729254](https://blog.csdn.net/why15732625998/article/details/78729254)
124 | 3. 感谢袋🐴饲养员的springboot(七).springboot整合jedis实现redis缓存:[http://www.cnblogs.com/GodHeng/p/9301330.html](http://www.cnblogs.com/GodHeng/p/9301330.html)
125 | 4. 感谢W_Z_W_888的spring注入静态变量的三种方法及其注意事项:[https://blog.csdn.net/W_Z_W_888/article/details/79979103](https://blog.csdn.net/W_Z_W_888/article/details/79979103)
126 | 5. 感谢天降风云的Vue2.0+ElementUI+PageHelper实现的表格分页:[https://blog.csdn.net/u012907049/article/details/70237457](https://blog.csdn.net/u012907049/article/details/70237457)
127 | 6. 感谢yaxx的Vuejs之axios获取Http响应头:[https://segmentfault.com/a/1190000009125333](https://segmentfault.com/a/1190000009125333)
128 | 7. 感谢Twilight的解决使用jwt刷新token带来的问题:[https://segmentfault.com/a/1190000013151506](https://segmentfault.com/a/1190000013151506)
129 | 8. 感谢chuhx的shiro拦截器,返回json数据:[https://blog.csdn.net/chuhx/article/details/51148877](https://blog.csdn.net/chuhx/article/details/51148877)
130 | 9. 感谢yidao620c的Shiro自带拦截器配置规则:[https://github.com/yidao620c/SpringBootBucket/tree/master/springboot-jwt](https://github.com/yidao620c/SpringBootBucket/tree/master/springboot-jwt)
131 |
132 | #### 参与贡献
133 |
134 | 1. Fork 本项目
135 | 2. 新建 Feat_xxx 分支
136 | 3. 提交代码
137 | 4. 新建 Pull Request
138 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | com.wang
7 | ShiroJwt
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | ShiroJwt
12 | Demo project for Spring Boot
13 |
14 |
15 | org.springframework.boot
16 | spring-boot-starter-parent
17 | 1.5.12.RELEASE
18 |
19 |
20 |
21 |
22 | UTF-8
23 | UTF-8
24 | 1.8
25 | 1.3.1
26 | 1.1.9
27 | 1.2.3
28 | 1.2.3
29 | 1.2.47
30 | 1.3.2
31 | 3.3.0
32 | 2.9.0
33 |
34 |
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter
39 |
40 |
41 |
42 | org.springframework.boot
43 | spring-boot-starter-test
44 | test
45 |
46 |
47 |
48 | org.springframework.boot
49 | spring-boot-devtools
50 | true
51 |
52 |
53 |
54 |
55 | org.springframework.boot
56 | spring-boot-starter-web
57 |
58 |
59 |
60 |
61 | mysql
62 | mysql-connector-java
63 |
64 |
65 |
66 |
67 | org.mybatis.spring.boot
68 | mybatis-spring-boot-starter
69 | ${mybatis.version}
70 |
71 |
72 |
73 |
74 | com.alibaba
75 | druid-spring-boot-starter
76 | ${druid.version}
77 |
78 |
79 |
80 |
81 | com.github.pagehelper
82 | pagehelper-spring-boot-starter
83 | ${pagehelper.version}
84 |
85 |
86 |
87 |
88 | tk.mybatis
89 | mapper-spring-boot-starter
90 | ${mapper.version}
91 |
92 |
93 |
94 |
95 | com.alibaba
96 | fastjson
97 | ${fastjson.version}
98 |
99 |
100 |
101 |
102 | org.apache.shiro
103 | shiro-spring
104 | ${shiro.version}
105 |
106 |
107 |
108 |
109 | com.auth0
110 | java-jwt
111 | ${jwt.version}
112 |
113 |
114 |
115 |
116 | redis.clients
117 | jedis
118 | ${jedis.version}
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | org.springframework.boot
128 | spring-boot-maven-plugin
129 |
130 | true
131 |
132 |
133 |
134 |
135 | org.apache.maven.plugins
136 | maven-compiler-plugin
137 |
138 | ${java.version}
139 | ${java.version}
140 | ${project.build.sourceEncoding}
141 |
142 |
143 |
144 |
145 | org.apache.maven.plugins
146 | maven-javadoc-plugin
147 | 3.0.0
148 |
149 |
150 |
151 | org.mybatis.generator
152 | mybatis-generator-maven-plugin
153 | 1.3.6
154 |
155 |
156 | src/main/resources/generator/generatorConfig.xml
157 |
158 | true
159 | true
160 |
161 |
162 |
163 | mysql
164 | mysql-connector-java
165 | 5.1.46
166 |
167 |
168 | tk.mybatis
169 | mapper-spring-boot-starter
170 | ${mapper.version}
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
--------------------------------------------------------------------------------
/src/main/java/com/wang/Application.java:
--------------------------------------------------------------------------------
1 | package com.wang;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | /**
7 | *
8 | * @author Wang926454
9 | * @date 2018/8/9 15:42
10 | */
11 | @SpringBootApplication
12 | @tk.mybatis.spring.annotation.MapperScan("com.wang.mapper")
13 | public class Application {
14 | public static void main(String[] args) {
15 | SpringApplication.run(Application.class, args);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/wang/config/ExceptionAdvice.java:
--------------------------------------------------------------------------------
1 | package com.wang.config;
2 |
3 | import com.wang.exception.CustomException;
4 | import com.wang.exception.CustomUnauthorizedException;
5 | import com.wang.model.common.ResponseBean;
6 | import org.apache.shiro.ShiroException;
7 | import org.apache.shiro.authz.UnauthenticatedException;
8 | import org.apache.shiro.authz.UnauthorizedException;
9 | import org.springframework.http.HttpStatus;
10 | import org.springframework.validation.BindException;
11 | import org.springframework.validation.FieldError;
12 | import org.springframework.web.bind.MethodArgumentNotValidException;
13 | import org.springframework.web.bind.annotation.ExceptionHandler;
14 | import org.springframework.web.bind.annotation.ResponseStatus;
15 | import org.springframework.web.bind.annotation.RestControllerAdvice;
16 | import org.springframework.web.servlet.NoHandlerFoundException;
17 |
18 | import javax.servlet.http.HttpServletRequest;
19 | import java.util.ArrayList;
20 | import java.util.HashMap;
21 | import java.util.List;
22 | import java.util.Map;
23 |
24 | /**
25 | * 异常控制处理器
26 | * @author dolyw.com
27 | * @date 2018/8/30 14:02
28 | */
29 | @RestControllerAdvice
30 | public class ExceptionAdvice {
31 | /**
32 | * 捕捉所有Shiro异常
33 | * @param e
34 | * @return
35 | */
36 | @ResponseStatus(HttpStatus.UNAUTHORIZED)
37 | @ExceptionHandler(ShiroException.class)
38 | public ResponseBean handle401(ShiroException e) {
39 | return new ResponseBean(HttpStatus.UNAUTHORIZED.value(), "无权访问(Unauthorized):" + e.getMessage(), null);
40 | }
41 |
42 | /**
43 | * 单独捕捉Shiro(UnauthorizedException)异常
44 | * 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常
45 | * @param e
46 | * @return
47 | */
48 | @ResponseStatus(HttpStatus.UNAUTHORIZED)
49 | @ExceptionHandler(UnauthorizedException.class)
50 | public ResponseBean handle401(UnauthorizedException e) {
51 | return new ResponseBean(HttpStatus.UNAUTHORIZED.value(), "无权访问(Unauthorized):当前Subject没有此请求所需权限(" + e.getMessage() + ")", null);
52 | }
53 |
54 | /**
55 | * 单独捕捉Shiro(UnauthenticatedException)异常
56 | * 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常
57 | * @param e
58 | * @return
59 | */
60 | @ResponseStatus(HttpStatus.UNAUTHORIZED)
61 | @ExceptionHandler(UnauthenticatedException.class)
62 | public ResponseBean handle401(UnauthenticatedException e) {
63 | return new ResponseBean(HttpStatus.UNAUTHORIZED.value(), "无权访问(Unauthorized):当前Subject是匿名Subject,请先登录(This subject is anonymous.)", null);
64 | }
65 |
66 | /**
67 | * 捕捉UnauthorizedException自定义异常
68 | * @return
69 | */
70 | @ResponseStatus(HttpStatus.UNAUTHORIZED)
71 | @ExceptionHandler(CustomUnauthorizedException.class)
72 | public ResponseBean handle401(CustomUnauthorizedException e) {
73 | return new ResponseBean(HttpStatus.UNAUTHORIZED.value(), "无权访问(Unauthorized):" + e.getMessage(), null);
74 | }
75 |
76 | /**
77 | * 捕捉校验异常(BindException)
78 | * @return
79 | */
80 | @ResponseStatus(HttpStatus.BAD_REQUEST)
81 | @ExceptionHandler(BindException.class)
82 | public ResponseBean validException(BindException e) {
83 | List fieldErrors = e.getBindingResult().getFieldErrors();
84 | Map result = this.getValidError(fieldErrors);
85 | return new ResponseBean(HttpStatus.BAD_REQUEST.value(), result.get("errorMsg").toString(), result.get("errorList"));
86 | }
87 |
88 | /**
89 | * 捕捉校验异常(MethodArgumentNotValidException)
90 | * @return
91 | */
92 | @ResponseStatus(HttpStatus.BAD_REQUEST)
93 | @ExceptionHandler(MethodArgumentNotValidException.class)
94 | public ResponseBean validException(MethodArgumentNotValidException e) {
95 | List fieldErrors = e.getBindingResult().getFieldErrors();
96 | Map result = this.getValidError(fieldErrors);
97 | return new ResponseBean(HttpStatus.BAD_REQUEST.value(), result.get("errorMsg").toString(), result.get("errorList"));
98 | }
99 |
100 | /**
101 | * 捕捉其他所有自定义异常
102 | * @return
103 | */
104 | @ResponseStatus(HttpStatus.BAD_REQUEST)
105 | @ExceptionHandler(CustomException.class)
106 | public ResponseBean handle(CustomException e) {
107 | return new ResponseBean(HttpStatus.BAD_REQUEST.value(), e.getMessage(), null);
108 | }
109 |
110 | /**
111 | * 捕捉404异常
112 | * @return
113 | */
114 | @ResponseStatus(HttpStatus.NOT_FOUND)
115 | @ExceptionHandler(NoHandlerFoundException.class)
116 | public ResponseBean handle(NoHandlerFoundException e) {
117 | return new ResponseBean(HttpStatus.NOT_FOUND.value(), e.getMessage(), null);
118 | }
119 |
120 | /**
121 | * 捕捉其他所有异常
122 | * @param request
123 | * @param ex
124 | * @return
125 | */
126 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
127 | @ExceptionHandler(Exception.class)
128 | public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
129 | return new ResponseBean(this.getStatus(request).value(), ex.toString() + ": " + ex.getMessage(), null);
130 | }
131 |
132 | /**
133 | * 获取状态码
134 | * @param request
135 | * @return
136 | */
137 | private HttpStatus getStatus(HttpServletRequest request) {
138 | Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
139 | if (statusCode == null) {
140 | return HttpStatus.INTERNAL_SERVER_ERROR;
141 | }
142 | return HttpStatus.valueOf(statusCode);
143 | }
144 |
145 | /**
146 | * 获取校验错误信息
147 | * @param fieldErrors
148 | * @return
149 | */
150 | private Map getValidError(List fieldErrors) {
151 | Map result = new HashMap(16);
152 | List errorList = new ArrayList();
153 | StringBuffer errorMsg = new StringBuffer("校验异常(ValidException):");
154 | for (FieldError error : fieldErrors) {
155 | errorList.add(error.getField() + "-" + error.getDefaultMessage());
156 | errorMsg.append(error.getField()).append("-").append(error.getDefaultMessage()).append(".");
157 | }
158 | result.put("errorList", errorList);
159 | result.put("errorMsg", errorMsg);
160 | return result;
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/main/java/com/wang/config/OriginFilter.java:
--------------------------------------------------------------------------------
1 | package com.wang.config;
2 |
3 | import org.apache.shiro.web.util.WebUtils;
4 | import org.springframework.stereotype.Component;
5 |
6 | import javax.servlet.*;
7 | import javax.servlet.http.HttpServletRequest;
8 | import javax.servlet.http.HttpServletResponse;
9 | import java.io.IOException;
10 |
11 | /**
12 | * 全局跨域放开
13 | *
14 | * @author wliduo[i@dolyw.com]
15 | * @date 2019/11/26 14:29
16 | */
17 | @Component
18 | public class OriginFilter implements Filter {
19 |
20 | @Override
21 | public void init(FilterConfig filterConfig) throws ServletException { }
22 |
23 | @Override
24 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
25 | HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
26 | HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
27 | httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
28 | httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
29 | httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
30 | httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
31 | filterChain.doFilter(request, response);
32 | }
33 |
34 | @Override
35 | public void destroy() { }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/wang/config/redis/JedisConfig.java:
--------------------------------------------------------------------------------
1 | package com.wang.config.redis;
2 |
3 | import com.wang.util.common.StringUtil;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
8 | import org.springframework.boot.context.properties.ConfigurationProperties;
9 | import org.springframework.context.annotation.Bean;
10 | import org.springframework.context.annotation.Configuration;
11 | import org.springframework.context.annotation.PropertySource;
12 | import redis.clients.jedis.JedisPool;
13 | import redis.clients.jedis.JedisPoolConfig;
14 |
15 | /**
16 | * Jedis配置,项目启动注入JedisPool
17 | * http://www.cnblogs.com/GodHeng/p/9301330.html
18 | * @author dolyw.com
19 | * @date 2018/9/5 10:35
20 | */
21 | @Configuration
22 | @EnableAutoConfiguration
23 | @PropertySource("classpath:config.properties")
24 | @ConfigurationProperties(prefix = "redis")
25 | public class JedisConfig {
26 |
27 | /**
28 | * logger
29 | */
30 | private static final Logger logger = LoggerFactory.getLogger(JedisConfig.class);
31 |
32 | private String host;
33 |
34 | private int port;
35 |
36 | private String password;
37 |
38 | private int timeout;
39 |
40 | @Value("${redis.pool.max-active}")
41 | private int maxActive;
42 |
43 | @Value("${redis.pool.max-wait}")
44 | private int maxWait;
45 |
46 | @Value("${redis.pool.max-idle}")
47 | private int maxIdle;
48 |
49 | @Value("${redis.pool.min-idle}")
50 | private int minIdle;
51 |
52 | @Bean
53 | public JedisPool redisPoolFactory() {
54 | try {
55 | JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
56 | jedisPoolConfig.setMaxIdle(maxIdle);
57 | jedisPoolConfig.setMaxWaitMillis(maxWait);
58 | jedisPoolConfig.setMaxTotal(maxActive);
59 | jedisPoolConfig.setMinIdle(minIdle);
60 | // 密码为空设置为null
61 | if (StringUtil.isBlank(password)) {
62 | password = null;
63 | }
64 | JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
65 | logger.info("初始化Redis连接池JedisPool成功!地址: {}:{}", host, port);
66 | return jedisPool;
67 | } catch (Exception e) {
68 | logger.error("初始化Redis连接池JedisPool异常:{}", e.getMessage());
69 | }
70 | return null;
71 | }
72 |
73 | public String getHost() {
74 | return host;
75 | }
76 |
77 | public void setHost(String host) {
78 | this.host = host;
79 | }
80 |
81 | public int getPort() {
82 | return port;
83 | }
84 |
85 | public void setPort(int port) {
86 | this.port = port;
87 | }
88 |
89 | public String getPassword() {
90 | return password;
91 | }
92 |
93 | public void setPassword(String password) {
94 | this.password = password;
95 | }
96 |
97 | public int getTimeout() {
98 | return timeout;
99 | }
100 |
101 | public void setTimeout(int timeout) {
102 | this.timeout = timeout;
103 | }
104 |
105 | public int getMaxActive() {
106 | return maxActive;
107 | }
108 |
109 | public void setMaxActive(int maxActive) {
110 | this.maxActive = maxActive;
111 | }
112 |
113 | public int getMaxWait() {
114 | return maxWait;
115 | }
116 |
117 | public void setMaxWait(int maxWait) {
118 | this.maxWait = maxWait;
119 | }
120 |
121 | public int getMaxIdle() {
122 | return maxIdle;
123 | }
124 |
125 | public void setMaxIdle(int maxIdle) {
126 | this.maxIdle = maxIdle;
127 | }
128 |
129 | public int getMinIdle() {
130 | return minIdle;
131 | }
132 |
133 | public void setMinIdle(int minIdle) {
134 | this.minIdle = minIdle;
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/main/java/com/wang/config/shiro/ShiroConfig.java:
--------------------------------------------------------------------------------
1 | package com.wang.config.shiro;
2 |
3 | import com.wang.config.shiro.jwt.JwtFilter;
4 | import com.wang.config.shiro.cache.CustomCacheManager;
5 | import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
6 | import org.apache.shiro.mgt.DefaultSubjectDAO;
7 | import org.apache.shiro.spring.LifecycleBeanPostProcessor;
8 | import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
9 | import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
10 | import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
11 | import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
12 | import org.springframework.context.annotation.Bean;
13 | import org.springframework.context.annotation.Configuration;
14 | import org.springframework.context.annotation.DependsOn;
15 |
16 | import javax.servlet.Filter;
17 | import java.util.HashMap;
18 | import java.util.LinkedHashMap;
19 | import java.util.Map;
20 |
21 | /**
22 | * Shiro配置
23 | * @author dolyw.com
24 | * @date 2018/8/30 15:49
25 | */
26 | @Configuration
27 | public class ShiroConfig {
28 |
29 | /**
30 | * 配置使用自定义Realm,关闭Shiro自带的session
31 | * 详情见文档 http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
32 | * @param userRealm
33 | * @return org.apache.shiro.web.mgt.DefaultWebSecurityManager
34 | * @author dolyw.com
35 | * @date 2018/8/31 10:55
36 | */
37 | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
38 | @Bean("securityManager")
39 | public DefaultWebSecurityManager defaultWebSecurityManager(UserRealm userRealm) {
40 | DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
41 | // 使用自定义Realm
42 | defaultWebSecurityManager.setRealm(userRealm);
43 | // 关闭Shiro自带的session
44 | DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
45 | DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
46 | defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
47 | subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
48 | defaultWebSecurityManager.setSubjectDAO(subjectDAO);
49 | // 设置自定义Cache缓存
50 | defaultWebSecurityManager.setCacheManager(new CustomCacheManager());
51 | return defaultWebSecurityManager;
52 | }
53 |
54 | /**
55 | * 添加自己的过滤器,自定义url规则
56 | * Shiro自带拦截器配置规则
57 | * rest:比如/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等
58 | * port:比如/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数
59 | * perms:比如/admins/user/**=perms[user:add:*],perms参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,比如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法
60 | * roles:比如/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,比如/admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。//要实现or的效果看http://zgzty.blog.163.com/blog/static/83831226201302983358670/
61 | * anon:比如/admins/**=anon 没有参数,表示可以匿名使用
62 | * authc:比如/admins/user/**=authc表示需要认证才能使用,没有参数
63 | * authcBasic:比如/admins/user/**=authcBasic没有参数表示httpBasic认证
64 | * ssl:比如/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
65 | * user:比如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查
66 | * 详情见文档 http://shiro.apache.org/web.html#urls-
67 | * @param securityManager
68 | * @return org.apache.shiro.spring.web.ShiroFilterFactoryBean
69 | * @author dolyw.com
70 | * @date 2018/8/31 10:57
71 | */
72 | @Bean("shiroFilter")
73 | public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
74 | ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
75 | // 添加自己的过滤器取名为jwt
76 | Map filterMap = new HashMap<>(16);
77 | filterMap.put("jwt", new JwtFilter());
78 | factoryBean.setFilters(filterMap);
79 | factoryBean.setSecurityManager(securityManager);
80 | // 自定义url规则使用LinkedHashMap有序Map
81 | LinkedHashMap filterChainDefinitionMap = new LinkedHashMap(16);
82 | // Swagger接口文档
83 | // filterChainDefinitionMap.put("/v2/api-docs", "anon");
84 | // filterChainDefinitionMap.put("/webjars/**", "anon");
85 | // filterChainDefinitionMap.put("/swagger-resources/**", "anon");
86 | // filterChainDefinitionMap.put("/swagger-ui.html", "anon");
87 | // filterChainDefinitionMap.put("/doc.html", "anon");
88 | // 公开接口
89 | // filterChainDefinitionMap.put("/api/**", "anon");
90 | // 登录接口放开
91 | filterChainDefinitionMap.put("/user/login", "anon");
92 | // 所有请求通过我们自己的JWTFilter
93 | filterChainDefinitionMap.put("/**", "jwt");
94 | factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
95 | return factoryBean;
96 | }
97 |
98 | /**
99 | * 下面的代码是添加注解支持
100 | */
101 | @Bean
102 | @DependsOn("lifecycleBeanPostProcessor")
103 | public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
104 | DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
105 | // 强制使用cglib,防止重复代理和可能引起代理出错的问题,https://zhuanlan.zhihu.com/p/29161098
106 | defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
107 | return defaultAdvisorAutoProxyCreator;
108 | }
109 |
110 | @Bean
111 | public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
112 | return new LifecycleBeanPostProcessor();
113 | }
114 |
115 | @Bean
116 | public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
117 | AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
118 | advisor.setSecurityManager(securityManager);
119 | return advisor;
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/java/com/wang/config/shiro/UserRealm.java:
--------------------------------------------------------------------------------
1 | package com.wang.config.shiro;
2 |
3 | import com.wang.config.shiro.jwt.JwtToken;
4 | import com.wang.util.JedisUtil;
5 | import com.wang.mapper.PermissionMapper;
6 | import com.wang.mapper.RoleMapper;
7 | import com.wang.mapper.UserMapper;
8 | import com.wang.model.PermissionDto;
9 | import com.wang.model.RoleDto;
10 | import com.wang.model.UserDto;
11 | import com.wang.model.common.Constant;
12 | import com.wang.util.JwtUtil;
13 | import com.wang.util.common.StringUtil;
14 | import org.apache.shiro.authc.AuthenticationException;
15 | import org.apache.shiro.authc.AuthenticationInfo;
16 | import org.apache.shiro.authc.AuthenticationToken;
17 | import org.apache.shiro.authc.SimpleAuthenticationInfo;
18 | import org.apache.shiro.authz.AuthorizationInfo;
19 | import org.apache.shiro.authz.SimpleAuthorizationInfo;
20 | import org.apache.shiro.realm.AuthorizingRealm;
21 | import org.apache.shiro.subject.PrincipalCollection;
22 | import org.springframework.beans.factory.annotation.Autowired;
23 | import org.springframework.stereotype.Service;
24 |
25 | import java.util.List;
26 |
27 | /**
28 | * 自定义Realm
29 | * @author dolyw.com
30 | * @date 2018/8/30 14:10
31 | */
32 | @Service
33 | public class UserRealm extends AuthorizingRealm {
34 |
35 | private final UserMapper userMapper;
36 | private final RoleMapper roleMapper;
37 | private final PermissionMapper permissionMapper;
38 |
39 | @Autowired
40 | public UserRealm(UserMapper userMapper, RoleMapper roleMapper, PermissionMapper permissionMapper) {
41 | this.userMapper = userMapper;
42 | this.roleMapper = roleMapper;
43 | this.permissionMapper = permissionMapper;
44 | }
45 |
46 | /**
47 | * 大坑,必须重写此方法,不然Shiro会报错
48 | */
49 | @Override
50 | public boolean supports(AuthenticationToken authenticationToken) {
51 | return authenticationToken instanceof JwtToken;
52 | }
53 |
54 | /**
55 | * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
56 | */
57 | @Override
58 | protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
59 | SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
60 | String account = JwtUtil.getClaim(principalCollection.toString(), Constant.ACCOUNT);
61 | UserDto userDto = new UserDto();
62 | userDto.setAccount(account);
63 | // 查询用户角色
64 | List roleDtos = roleMapper.findRoleByUser(userDto);
65 | for (RoleDto roleDto : roleDtos) {
66 | if (roleDto != null) {
67 | // 添加角色
68 | simpleAuthorizationInfo.addRole(roleDto.getName());
69 | // 根据用户角色查询权限
70 | List permissionDtos = permissionMapper.findPermissionByRole(roleDto);
71 | for (PermissionDto permissionDto : permissionDtos) {
72 | if (permissionDto != null) {
73 | // 添加权限
74 | simpleAuthorizationInfo.addStringPermission(permissionDto.getPerCode());
75 | }
76 | }
77 | }
78 | }
79 | return simpleAuthorizationInfo;
80 | }
81 |
82 | /**
83 | * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
84 | */
85 | @Override
86 | protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
87 | String token = (String) authenticationToken.getCredentials();
88 | // 解密获得account,用于和数据库进行对比
89 | String account = JwtUtil.getClaim(token, Constant.ACCOUNT);
90 | // 帐号为空
91 | if (StringUtil.isBlank(account)) {
92 | throw new AuthenticationException("Token中帐号为空(The account in Token is empty.)");
93 | }
94 | // 查询用户是否存在
95 | UserDto userDto = new UserDto();
96 | userDto.setAccount(account);
97 | userDto = userMapper.selectOne(userDto);
98 | if (userDto == null) {
99 | throw new AuthenticationException("该帐号不存在(The account does not exist.)");
100 | }
101 | // 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
102 | if (JwtUtil.verify(token) && JedisUtil.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
103 | // 获取RefreshToken的时间戳
104 | String currentTimeMillisRedis = JedisUtil.getObject(Constant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString();
105 | // 获取AccessToken时间戳,与RefreshToken的时间戳对比
106 | if (JwtUtil.getClaim(token, Constant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
107 | return new SimpleAuthenticationInfo(token, token, "userRealm");
108 | }
109 | }
110 | throw new AuthenticationException("Token已过期(Token expired or incorrect.)");
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/main/java/com/wang/config/shiro/cache/CustomCache.java:
--------------------------------------------------------------------------------
1 | package com.wang.config.shiro.cache;
2 |
3 | import com.wang.util.JwtUtil;
4 | import com.wang.util.JedisUtil;
5 | import com.wang.model.common.Constant;
6 | import com.wang.util.common.PropertiesUtil;
7 | import com.wang.util.common.SerializableUtil;
8 | import org.apache.shiro.cache.Cache;
9 | import org.apache.shiro.cache.CacheException;
10 |
11 | import java.util.*;
12 |
13 | /**
14 | * 重写Shiro的Cache保存读取
15 | * @author dolyw.com
16 | * @date 2018/9/4 17:31
17 | */
18 | public class CustomCache implements Cache {
19 |
20 | /**
21 | * 缓存的key名称获取为shiro:cache:account
22 | * @param key
23 | * @return java.lang.String
24 | * @author dolyw.com
25 | * @date 2018/9/4 18:33
26 | */
27 | private String getKey(Object key) {
28 | return Constant.PREFIX_SHIRO_CACHE + JwtUtil.getClaim(key.toString(), Constant.ACCOUNT);
29 | }
30 |
31 | /**
32 | * 获取缓存
33 | */
34 | @Override
35 | public Object get(Object key) throws CacheException {
36 | if(Boolean.FALSE.equals(JedisUtil.exists(this.getKey(key)))){
37 | return null;
38 | }
39 | return JedisUtil.getObject(this.getKey(key));
40 | }
41 |
42 | /**
43 | * 保存缓存
44 | */
45 | @Override
46 | public Object put(Object key, Object value) throws CacheException {
47 | // 读取配置文件,获取Redis的Shiro缓存过期时间
48 | PropertiesUtil.readProperties("config.properties");
49 | String shiroCacheExpireTime = PropertiesUtil.getProperty("shiroCacheExpireTime");
50 | // 设置Redis的Shiro缓存
51 | return JedisUtil.setObject(this.getKey(key), value, Integer.parseInt(shiroCacheExpireTime));
52 | }
53 |
54 | /**
55 | * 移除缓存
56 | */
57 | @Override
58 | public Object remove(Object key) throws CacheException {
59 | if(Boolean.FALSE.equals(JedisUtil.exists(this.getKey(key)))){
60 | return null;
61 | }
62 | JedisUtil.delKey(this.getKey(key));
63 | return null;
64 | }
65 |
66 | /**
67 | * 清空所有缓存
68 | */
69 | @Override
70 | public void clear() throws CacheException {
71 | Objects.requireNonNull(JedisUtil.getJedis()).flushDB();
72 | }
73 |
74 | /**
75 | * 缓存的个数
76 | */
77 | @Override
78 | public int size() {
79 | Long size = Objects.requireNonNull(JedisUtil.getJedis()).dbSize();
80 | return size.intValue();
81 | }
82 |
83 | /**
84 | * 获取所有的key
85 | */
86 | @Override
87 | public Set keys() {
88 | Set keys = Objects.requireNonNull(JedisUtil.getJedis()).keys("*".getBytes());
89 | Set