Skip to content

Commit 6504250

Browse files
authored
Create shiro 整合 springBoot 实现基本的角色权限控制.md
1 parent 0b20090 commit 6504250

File tree

1 file changed

+337
-0
lines changed

1 file changed

+337
-0
lines changed
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
## 依赖包
2+
3+
```
4+
<dependency>
5+
<groupId>org.apache.shiro</groupId>
6+
<artifactId>shiro-spring</artifactId>
7+
<version>1.3.2</version>
8+
</dependency>
9+
```
10+
## 数据库表
11+
一切从简,用户 user 表,以及角色 role 表
12+
![user](https://upload-images.jianshu.io/upload_images/8807674-14c71a60da237fbf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
13+
14+
![role](https://upload-images.jianshu.io/upload_images/8807674-9bfb0a984fc9c28c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
15+
16+
17+
## Shiro 相关类
18+
### Shiro 配置类
19+
```
20+
@Configuration
21+
public class ShiroConfig {
22+
@Bean
23+
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
24+
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
25+
// 必须设置 SecurityManager
26+
shiroFilterFactoryBean.setSecurityManager(securityManager);
27+
// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
28+
shiroFilterFactoryBean.setLoginUrl("/notLogin");
29+
// 设置无权限时跳转的 url;
30+
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
31+
32+
// 设置拦截器
33+
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
34+
//游客,开发权限
35+
filterChainDefinitionMap.put("/guest/**", "anon");
36+
//用户,需要角色权限 “user”
37+
filterChainDefinitionMap.put("/user/**", "roles[user]");
38+
//管理员,需要角色权限 “admin”
39+
filterChainDefinitionMap.put("/admin/**", "roles[admin]");
40+
//开放登陆接口
41+
filterChainDefinitionMap.put("/login", "anon");
42+
//其余接口一律拦截
43+
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
44+
filterChainDefinitionMap.put("/**", "authc");
45+
46+
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
47+
System.out.println("Shiro拦截器工厂类注入成功");
48+
return shiroFilterFactoryBean;
49+
}
50+
51+
/**
52+
* 注入 securityManager
53+
*/
54+
@Bean
55+
public SecurityManager securityManager() {
56+
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
57+
// 设置realm.
58+
securityManager.setRealm(customRealm());
59+
return securityManager;
60+
}
61+
62+
/**
63+
* 自定义身份认证 realm;
64+
* <p>
65+
* 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
66+
* 否则会影响 CustomRealm类 中其他类的依赖注入
67+
*/
68+
@Bean
69+
public CustomRealm customRealm() {
70+
return new CustomRealm();
71+
}
72+
}
73+
```
74+
**注意**:里面的 SecurityManager 类导入的应该是 ```import org.apache.shiro.mgt.SecurityManager;``` 但是,如果你是复制代码过来的话,会默认导入 ```java.lang.SecurityManager``` 这里也稍稍有点坑,其他的类的话,也是都属于 shiro 包里面的类
75+
76+
shirFilter 方法中主要是设置了一些重要的跳转 url,比如未登陆时,无权限时的跳转;以及设置了各类 url 的权限拦截,比如 /user 开始的 url 需要 user 权限,/admin 开始的 url 需要 admin 权限等
77+
78+
#### 权限拦截 Filter
79+
80+
当运行一个Web应用程序时,Shiro将会创建一些有用的默认 Filter 实例,并自动地将它们置为可用,而这些默认的 Filter 实例是被 DefaultFilter 枚举类定义的,当然我们也可以自定义 Filter 实例,这些在以后的文章中会讲到
81+
![DefaultFilter](https://upload-images.jianshu.io/upload_images/8807674-6ef51f395cf8c6b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
82+
83+
Filter | 解释
84+
---|---
85+
anon | 无参,开放权限,可以理解为匿名用户或游客
86+
authc | 无参,需要认证
87+
logout | 无参,注销,执行后会直接跳转到``` shiroFilterFactoryBean.setLoginUrl(); ``` 设置的 url
88+
authcBasic | 无参,表示 httpBasic 认证
89+
user | 无参,表示必须存在用户,当登入操作时不做检查
90+
ssl | 无参,表示安全的URL请求,协议为 https
91+
perms[user] | 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过
92+
roles[admin] | 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
93+
rest[user] |根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
94+
port[8081] | 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数
95+
96+
常用的主要就是 anon,authc,user,roles,perms 等
97+
98+
**注意**:anon, authc, authcBasic, user 是第一组认证过滤器,perms, port, rest, roles, ssl 是第二组授权过滤器,要通过授权过滤器,就先要完成登陆认证操作(即先要完成认证才能前去寻找授权) 才能走第二组授权器(例如访问需要 roles 权限的 url,如果还没有登陆的话,会直接跳转到 ``` shiroFilterFactoryBean.setLoginUrl(); ``` 设置的 url )
99+
100+
101+
102+
103+
104+
105+
106+
107+
### 自定义 realm 类
108+
我们首先要继承 AuthorizingRealm 类来自定义我们自己的 realm 以进行我们自定义的身份,权限认证操作。
109+
记得要 Override 重写 doGetAuthenticationInfo 和 doGetAuthorizationInfo 两个方法(两个方法名很相似,不要搞错)
110+
```
111+
public class CustomRealm extends AuthorizingRealm {
112+
private UserMapper userMapper;
113+
114+
@Autowired
115+
private void setUserMapper(UserMapper userMapper) {
116+
this.userMapper = userMapper;
117+
}
118+
119+
/**
120+
* 获取身份验证信息
121+
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
122+
*
123+
* @param authenticationToken 用户身份信息 token
124+
* @return 返回封装了用户信息的 AuthenticationInfo 实例
125+
*/
126+
@Override
127+
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
128+
System.out.println("————身份认证方法————");
129+
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
130+
// 从数据库获取对应用户名密码的用户
131+
String password = userMapper.getPassword(token.getUsername());
132+
if (null == password) {
133+
throw new AccountException("用户名不正确");
134+
} else if (!password.equals(new String((char[]) token.getCredentials()))) {
135+
throw new AccountException("密码不正确");
136+
}
137+
return new SimpleAuthenticationInfo(token.getPrincipal(), password, getName());
138+
}
139+
140+
/**
141+
* 获取授权信息
142+
*
143+
* @param principalCollection
144+
* @return
145+
*/
146+
@Override
147+
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
148+
System.out.println("————权限认证————");
149+
String username = (String) SecurityUtils.getSubject().getPrincipal();
150+
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
151+
//获得该用户角色
152+
String role = userMapper.getRole(username);
153+
Set<String> set = new HashSet<>();
154+
//需要将 role 封装到 Set 作为 info.setRoles() 的参数
155+
set.add(role);
156+
//设置该用户拥有的角色
157+
info.setRoles(set);
158+
return info;
159+
}
160+
}
161+
```
162+
重写的两个方法分别是实现身份认证以及权限认证,shiro 中有个作登陆操作的 ``` Subject.login() ``` 方法,当我们把封装了用户名,密码的 token 作为参数传入,便会跑进这两个方法里面(不一定两个方法都会进入)
163+
164+
其中 doGetAuthorizationInfo 方法只有在需要权限认证时才会进去,比如前面配置类中配置了 ``` filterChainDefinitionMap.put("/admin/**", "roles[admin]");``` 的管理员角色,这时进入 /admin 时就会进入 doGetAuthorizationInfo 方法来检查权限;而 doGetAuthenticationInfo 方法则是需要身份认证时(比如前面的 ```Subject.login()``` 方法)才会进入
165+
166+
再说下 UsernamePasswordToken 类,我们可以从该对象拿到登陆时的用户名和密码(登陆时会使用 ```new UsernamePasswordToken(username, password);```),而 get 用户名或密码有以下几个方法
167+
```
168+
token.getUsername() //获得用户名 String
169+
token.getPrincipal() //获得用户名 Object
170+
token.getPassword() //获得密码 char[]
171+
token.getCredentials() //获得密码 Object
172+
```
173+
174+
**注意**:有很多人会发现,UserMapper 等类,接口无法通过 @Autowired 注入进来,跑程序的时候会报 NullPointerException,网上说了很多诸如是 Spring 加载顺序等原因,但其实有一个很重要的地方要大家注意,CustomRealm 这个类是在 shiro 配置类的 ```securityManager.setRealm() ``` 方法中设置进去的,而很多人直接写```securityManager.setRealm(new CustomRealm());``` ,这样是不行的,必须要使用 @Bean 注入 MyRealm,不能直接 new 对象:
175+
```
176+
@Bean
177+
public SecurityManager securityManager() {
178+
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
179+
// 设置realm.
180+
securityManager.setRealm(customRealm());
181+
return securityManager;
182+
}
183+
184+
@Bean
185+
public CustomRealm customRealm() {
186+
return new CustomRealm();
187+
}
188+
```
189+
道理也很简单,和 Controller 中调用 Service 一样,都是 SpringBean,不能自己 new
190+
191+
当然,同样的道理也可以这样写:
192+
```
193+
@Bean
194+
public SecurityManager securityManager(CustomRealm customRealm) {
195+
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
196+
// 设置realm.
197+
securityManager.setRealm(customRealm);
198+
return securityManager;
199+
}
200+
```
201+
然后只要在 CustomRealm 类加上个类似 @Component 的注解即可
202+
203+
## 功能实现
204+
本文的功能全部以接口返回 json 数据的方式实现
205+
### 根据 url 权限分配 controller
206+
```
207+
游客
208+
@RestController
209+
@RequestMapping("/guest")
210+
public class GuestController{
211+
@Autowired
212+
private final ResultMap resultMap;
213+
214+
@RequestMapping(value = "/enter", method = RequestMethod.GET)
215+
public ResultMap login() {
216+
return resultMap.success().message("欢迎进入,您的身份是游客");
217+
}
218+
219+
@RequestMapping(value = "/getMessage", method = RequestMethod.GET)
220+
public ResultMap submitLogin() {
221+
return resultMap.success().message("您拥有获得该接口的信息的权限!");
222+
}
223+
}
224+
```
225+
```
226+
普通登陆用户
227+
@RestController
228+
@RequestMapping("/user")
229+
public class UserController{
230+
@Autowired
231+
private final ResultMap resultMap;
232+
233+
@RequestMapping(value = "/getMessage", method = RequestMethod.GET)
234+
public ResultMap getMessage() {
235+
return resultMap.success().message("您拥有用户权限,可以获得该接口的信息!");
236+
}
237+
}
238+
```
239+
```
240+
管理员
241+
@RestController
242+
@RequestMapping("/admin")
243+
public class AdminController {
244+
@Autowired
245+
private final ResultMap resultMap;
246+
247+
@RequestMapping(value = "/getMessage", method = RequestMethod.GET)
248+
public ResultMap getMessage() {
249+
return resultMap.success().message("您拥有管理员权限,可以获得该接口的信息!");
250+
}
251+
}
252+
```
253+
突然注意到 CustomRealm 类那里抛出了 AccountException 异常,现在建个类进行异常捕获
254+
```
255+
@RestControllerAdvice
256+
public class ExceptionController {
257+
private final ResultMap resultMap;
258+
259+
@Autowired
260+
public ExceptionController(ResultMap resultMap) {
261+
this.resultMap = resultMap;
262+
}
263+
264+
// 捕捉 CustomRealm 抛出的异常
265+
@ExceptionHandler(AccountException.class)
266+
public ResultMap handleShiroException(Exception ex) {
267+
return resultMap.fail().message(ex.getMessage());
268+
}
269+
}
270+
```
271+
还有进行登陆等处理的 LoginController
272+
```
273+
@RestController
274+
public class LoginController {
275+
@Autowired
276+
private ResultMap resultMap;
277+
private UserMapper userMapper;
278+
279+
@RequestMapping(value = "/notLogin", method = RequestMethod.GET)
280+
public ResultMap notLogin() {
281+
return resultMap.success().message("您尚未登陆!");
282+
}
283+
284+
@RequestMapping(value = "/notRole", method = RequestMethod.GET)
285+
public ResultMap notRole() {
286+
return resultMap.success().message("您没有权限!");
287+
}
288+
289+
@RequestMapping(value = "/logout", method = RequestMethod.GET)
290+
public ResultMap logout() {
291+
Subject subject = SecurityUtils.getSubject();
292+
//注销
293+
subject.logout();
294+
return resultMap.success().message("成功注销!");
295+
}
296+
297+
/**
298+
* 登陆
299+
*
300+
* @param username 用户名
301+
* @param password 密码
302+
*/
303+
@RequestMapping(value = "/login", method = RequestMethod.POST)
304+
public ResultMap login(String username, String password) {
305+
// 从SecurityUtils里边创建一个 subject
306+
Subject subject = SecurityUtils.getSubject();
307+
// 在认证提交前准备 token(令牌)
308+
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
309+
// 执行认证登陆
310+
subject.login(token);
311+
//根据权限,指定返回数据
312+
String role = userMapper.getRole(username);
313+
if ("user".equals(role)) {
314+
return resultMap.success().message("欢迎登陆");
315+
}
316+
if ("admin".equals(role)) {
317+
return resultMap.success().message("欢迎来到管理员页面");
318+
}
319+
return resultMap.fail().message("权限错误!");
320+
}
321+
}
322+
```
323+
### 测试
324+
![登陆前访问信息接口](https://upload-images.jianshu.io/upload_images/8807674-7048df42b04f5763.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
325+
326+
327+
![普通用户登陆](https://upload-images.jianshu.io/upload_images/8807674-52bda59ed16c2de4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
328+
329+
![密码错误](https://upload-images.jianshu.io/upload_images/8807674-cb0266faa390c80f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
330+
331+
![管理员登陆](https://upload-images.jianshu.io/upload_images/8807674-2b07ade4277c9f3d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
332+
333+
![获取信息](https://upload-images.jianshu.io/upload_images/8807674-86ce356a15a49615.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
334+
335+
![获取信息](https://upload-images.jianshu.io/upload_images/8807674-cb377665ca151889.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
336+
337+
![注销](https://upload-images.jianshu.io/upload_images/8807674-3fc0b74103c51b2c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

0 commit comments

Comments
 (0)