(六)Spring Boot整合Shiro

Spring Boot整合Shiro

:tada: :tada: :tada: 这里有丰富的 Spring 框架学习案例

仓库地址:spring-learn
欢迎star、fork,给作者一些鼓励

写在前面

之前有写过SSM整合Shiro的示例:SSM权限管理示例

而在Spring Boot中使用Shiro,就是需要把之前SSM的XML配置转换成Java代码配置,下面我举例用Spring Boot2.x + Shiro实现登录认证。

导入依赖

1
2
3
4
5
6
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

由于Shiro本身并没有提供缓存实现,这里使用Shiro官方支持的ehcache缓存:

Ehcache:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- ehcache缓存 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>

如果你想用Redis缓存,可以用这个封装好的插件:

Shiro-redis

1
2
3
4
5
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>

修改application.yml

1
2
3
4
5
6
7
8
9
10
11
datasource:
name: springboot
type: com.alibaba.druid.pool.DruidDataSource
#druid相关配置
druid:
#mysql驱动
driver-class-name: com.mysql.cj.jdbc.Driver
#基本属性
url: jdbc:mysql://127.0.0.1:3306/springboot_shiro?useUnicode=true&characterEncoding=UTF-8
username: root
password: root

更多的配置请看该项目下src/main/resources/application.yml

初始化数据库

1
2
3
4
5
6
7
8
9
10
-- create database springboot_shiro charset utf8;

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(255) DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

创建ShiroConfig.java

如之前在SSM整合Shiro框架时,Shiro的基础配置一般有如下:

  • SecurityManager: 安全管理器,Shiro的核心

  • Realm: Shiro从Realm中获取验证数据

  • SessionManager: 会话管理

于是,我们就大概知道ShiroConfig.java中大概需要配置什么信息了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Configuration
public class ShiroConfig {

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setLoginUrl("/login");

//自定义拦截器链
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/login", "anon");

//静态资源,对应`/resources/static`文件夹下的资源
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/lib/**", "anon");

//其他请求一律拦截,一般放在拦截器链的最后
//区分`user`和`authc`拦截器区别:`user`拦截器允许登录用户和RememberMe的用户访问
filterChainDefinitionMap.put("/**", "user");

filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return filterFactoryBean;
}

@Bean
public Realm realm() {
return new AuthRealm();
}

@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}

@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(60 * 60 * 10); //10分钟
sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
return sessionManager;
}
}

自定义Realm实现

上面是一个最基本的Shiro环境配置,其实这个XML中配置基本雷同的,相信你也发现了。

下面进行第二部:自定义Realm实现,创建AuthRealm.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class AuthRealm extends AuthorizingRealm {

@Autowired
private UserService userService;

/**
* 权限校验相关
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

/**
* 身份认证相关
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
/**
* 1. 从Token中获取输入的用户名密码
* 2. 通过输入的用户名查询数据库得到密码
* 3. 调用Authentication进行密码校验
*/

//获取用户名密码
String username = (String) authenticationToken.getPrincipal();
String password = new String((char[]) authenticationToken.getCredentials());

User user = userService.findByUsername(username);
if (user == null) {
throw new UnknownAccountException();
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException();
}
return new SimpleAuthenticationInfo(user, password, getName());
}
}

对于自定义Realm实现,我们仅需要继承AuthorizingRealm,看源码发现:

1
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {}

继承了一个抽象类,就应该实现重写它的抽象方法:

1
2
3
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection var1);

protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;
  • AuthorizationInfo用于权限校验
  • AuthenticationInfo用于身份验证

案例

上面配置好了Shiro环境,下面我们实践一下。

创建index.htmllogin.html两个页面:

创建LoginController.java,编写路由导航地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
public class LoginController {
private Logger logger = LoggerFactory.getLogger(this.getClass());

/**
* 首页地址
*
* @return
*/
@GetMapping(value = {"/", "/index"})
public String index() {
return "index";
}

/**
* 登录地址
*
* @return
*/
@GetMapping("/login")
public String login() {
return "login";
}
}

注意几点:

  • @Controller用来告诉Spring这是个处理HTTP请求的控制器。
  • @RestController@ResponseBody@Controller的组合,被标记的控制器类所有return数据都自动封装为JSON格式。
  • @GetMapping标记该请求是Get请求,如果用Post请求则会报错no support

启动项目,在浏览器上访问localhost:8080或者localhost:8080/index发现页面均会跳转到/login这个请求上:

细心地你会发现请求地址中可能会拼接一个JSESSIONID,并且所有的的请求中均会携带一个Cookie= JSESSIONID。这其实是Shiro用于身份验证用的,Shiro默认生成一个会话ID,并储存在Cookie中,这样浏览器每次的请求头中都将携带这个Cookie数据,Shiro拦截请求,发现这个Cookie值是有效的会话(Session) ID,就判定这个请求是合法的请求,然后再根据自定义拦截器链决定是否对该请求放行。

登录

编写一个form表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
<link rel="stylesheet" th:href="@{/css/login.css}"/>
</head>
<body>

<h1>登录页</h1>

<form method="post" action="/login">
<input type="text" name="username"/><br/>
<input type="password" name="password"><br/>
<input type="submit" value="登录">
</form>

<div class="info" th:text="${info}"></div>

</body>
</html>

编写后台接口 post /login

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 登录接口
*
* @param username 用户名
* @param password 密码
* @return 状态信息或成功页面视图地址
*/
@PostMapping("/login")
public String login(String username, String password, Model model) {
String info = null;

//封装Token信息=用户名+密码
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//获取Shiro Subject实例
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
info = String.valueOf(subject.isAuthenticated());
model.addAttribute("info", "登录状态 ==> " + info);
return "/index";
} catch (UnknownAccountException e) {
e.printStackTrace();
info = "未知账户异常";
} catch (AuthenticationException e) {
e.printStackTrace();
info = "账户名或密码错误";
} catch (Exception e) {
e.printStackTrace();
info = "其他异常";
}
model.addAttribute("info", "登录状态 ==> " + info);
logger.info("登录状态 ==> {}", info);
return "/login";
}

如上,前台form表单中的action="/login"method="post"决定了请求走这个地址,通过调用subject.login(token),Shiro自动查询Realm实现,于是找到我们自定义的Realm实现:AuthRealm,进而通过SimpleAuthenticationInfo方法验证了登录用户的身份,如果身份认证成功,就return "/index",否则就return "/login"

上面出现了两个/login接口:

1
2
3
4
@GetMapping("/login")
public String login() {
return "login";
}
1
2
@PostMapping("/login")
public String login(String username, String password, Model model) {}

这里就提现出了@GetMapping@PostMapping的优势,利用Java的方法重载创建了两个名称相同的接口,但是根据HTTP请求方法的不同(Get还是Post)会自动寻找对应的映射方法。

更多的Shiro特性可以参看我的这个项目:SSM权限管理示例

同时推荐大家阅读张开涛老师的:跟我学Shiro


交流

以上仅是个人的见解,可能有些地方是错误的,深知自己的菜鸡技术,欢迎大佬指出。

个人建了一个Java交流群:671017003。 欢迎大佬或是新人入驻一起交流学习Java技术。


联系

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

如果你觉得这篇文章帮助到了你,你可以帮作者买一杯果汁表示鼓励