Little eagle - SpringBoot实战管理系统(三)

背景

上一part中掌握了如何使用常用数据源,jdbc、druid和mybatis

在part1中为了防止用户在未登陆用户下访问到员工管理页面,用过滤器的方式实现了对该类请求的拦截。在本part中整合常用安全框架,以简单配置代替编码实现拦截器等其它安全功能。

整合框架及所实现功能移步左侧一级标题

项目地址(源码自取↓):

整合SpringSecurity:https://github.com/codersliu/springboot06-springsecurity

整合Shiro:https://github.com/codersliu/springboot08-shiro

整合SpringSecurity

新建项目,导入以下依赖↓

导入resources/static/qinjiang和resources/templates下的静态资源

application.properties下关闭thymeleaf缓存

1
spring.thymeleaf.cache=false

新建controller/RouteController实现页面跳转

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
package com.sliu.springboot06springsecurity.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class RouteController {

@RequestMapping({"/", "/index", "/index.html"})
public String index(){
return "index";
}

@RequestMapping("/toLogin")
public String toLogin(){
return "views/login";
}

@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") int id){
return "views/level1/"+id;
}

@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") int id){
return "views/level2/"+id;
}

@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") int id){
return "views/level3/"+id;
}
}

测试时要注释掉pom.xml下SpringSecurity依赖,引入依赖会自动实现用户登录状态的过滤。

最终页面如↓

测试完成后取取消SpringSecurity依赖注释

授权和认证

新建config/SecurityConfig类,实现WebSecurity

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
package com.sliu.springboot06springsecurity.config;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

//AOP原理实现拦截器
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

// 链式编程
// 授权
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置首页所有人可见,level页面对应权限可见
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");

//没有权限默认跳转登陆页面
http.formLogin();
}

// 认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

// 仿照数据库数据,真实应用场景下应从数据库读入
auth.inMemoryAuthentication()
.withUser("sliu").password("{noop}123456").roles("vip1", "vip2")
.and()
.withUser("root").password("{noop}123456").roles("vip1", "vip2", "vip3")
.and()
.withUser("guest").password("{noop}123456").roles("vip1");
}

}

注销和权限控制

该模块涉及前端知识角度,快速过一遍

config/SecurityConfig下开启注销功能(configure方法下添加)

1
2
//注销成功返回首页
http.logout().logoutSuccessUrl("/");

index.html中登陆按钮下添加注销按钮

1
2
3
<a class="item" th:href="@{/logout}">
<i class="address card icon"></i> 注销
</a>

注销搞定!

接下来完成在对应权限登陆后,仅现实当前vip权限能够访问的资源,例如:sliu权限为vip1和vip2,那么在登陆完成后,主页不应该显示level3相关可操作项。

pom.xml下导入thymeleaf-springSecurity整合包

1
2
3
4
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

修改index.html实现

在用户未登陆时仅显示登陆按钮,用户已登录显示用户名、注销按钮

导入命名空间

1
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

html下主要两处修改

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>首页</title>
<!--semantic-ui-->
<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
<link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet">
</head>
<body>

<!--主容器-->
<div class="ui container">

<div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
<div class="ui secondary menu">
<a class="item" th:href="@{/index}">首页</a>
<!--修改1-->
<!--登录注销-->
<div class="right menu">
<!-- 未登陆显示登陆按钮-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
<!-- 登陆成功显示用户名与注销按钮-->
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="address card icon"></i> 注销
</a>
</div>
<div sec:authorize="isAuthenticated()">
<a class="item">
用户名:<span sec:authentication="name"></span>
<!-- 角色:<span sec:authentication="principal.getAuthorities()"></span>-->
</a>
</div>
<!--已登录
<a th:href="@{/usr/toUserCenter}">
<i class="address card icon"></i> admin
</a>
-->
</div>
</div>
</div>

<div class="ui segment" style="text-align: center">
<h3>Spring Security Study by 秦疆</h3>
</div>

<div>
<br>
<div class="ui three column stackable grid">
<!--修改2-->
<div class="column" sec:authorize="hasRole('vip1')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>

<div class="column" sec:authorize="hasRole('vip2')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
</div>
</div>
</div>

<div class="column" sec:authorize="hasRole('vip3')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 3</h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
</div>
</div>
</div>
</div>

</div>
</div>

</div>


<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/qinjiang/js/semantic.min.js}"></script>

</body>
</html>

SecurityConfig下config方法中开启防止网站攻击

1
2
//        防止网站攻击:get&post,登陆失败可能存在的原因
http.csrf().disable();

最终效果图↓:

记住我和首页定制

开启remember me功能,通过cookie保存登陆状态,浏览器关闭后,下次访问无需再次登陆。

SecurityConfig类下config函数内添加

1
2
//        开启记住我,Cookie默认保存两周,同时为了整合自定义的首页,接收前端参数,用于判断remenber me功能在哪里触发
http.rememberMe().rememberMeParameter("remember");

首页定制在config函数下添加:

1
2
3
    //没有权限默认跳转登陆页面
// 自定义登录页
http.formLogin().loginPage("/toLogin").usernameParameter("user").passwordParameter("pwd").loginProcessingUrl("/login");

整合shiro

环境搭建

shiro官网下载安装包

以下三个概念对于理解shiro如何运行非常重要:

(1)Subject 用户

(2)SecurityManager 管理所有用户

(3)Realm 连接数据

1.导入shiro核心依赖

pom.xml中导入依赖

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

新建src/main/java/com.xxx.shirospringboot/config/ShiroConfig.java用于配置shiro

2.编写shiro核心配置

该类核心框架如下

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.sliu.shirospringboot.config;

import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {

//ShiroFilterFactoryBean

//DefaultWebSecurityManager

//创建realm对象,需要自定义类
}

依次从下向上实现对应模块

(1)创建realm对象

新建config/UserRealm.java自定义Realm

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
package com.sliu.shirospringboot.config;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

//自定义的UserRealm,继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了授权======>doGetAuthorizationInfo");
return null;
}

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证权======>doGetAuthenticationInfo");
return null;
}
}

(2)ShiroConfig中将自定义UserRealm放入容器Bean中

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
package com.sliu.shirospringboot.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {

//ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}

//DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(userRealm());
return securityManager;
}

//创建realm对象,需要自定义类
@Bean(name = "userRealm")
public UserRealm userRealm(){
return new UserRealm();
}
}

Shiro登陆拦截

要实现对用户的登陆拦截,避免未认证用户进入系统,shiro中需要对自定义配置ShiroConfig中的getShiroFilterFactoryBean中进行过滤器的配置

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
//ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);

//添加shiro内置过滤器
/*
anon: 无需认证即可访问
authc: 必须认证才能访问
user: 必须配置记住我 功能才能用
perms: 拥有对某个资源的权限才能访问
role: 拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();

//设置user下的资源必须认证才能访问
filterMap.put("/user/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);

//设置认证失败,登陆请求
bean.setLoginUrl("/login");
return bean;
}

Shiro用户认证

MyController.java内引入常规登陆规范检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("/login")
public String login(String username, String password, Model model){
//获取当前用户
Subject subject = SecurityUtils.getSubject();

//封装用户登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

//执行登陆方法,没有异常就ok了
try {
subject.login(token);
return "index";
}catch (UnknownAccountException e){//用户名不存在
model.addAttribute("msg", "用户名错误");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg", "密码错误");
return "login";
}

}

自定义UserRealm中配置认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证======>doGetAuthenticationInfo");

//用户名,密码 从数据库中取
String name = "root";
String password = "123456";

UsernamePasswordToken userToken = (UsernamePasswordToken) token;
if(!userToken.getUsername().equals(name)){
return null; //抛出异常 UnkonwnAccountException
}

//密码认证,shiro实现
return new SimpleAuthenticationInfo("",password,"");
}

整合mybatis

pom.xml中引入连接数据库依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

新建application.yml配置数据源

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
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/company?useUnicode=true&characterEnconding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的 Filter,去掉后监控界面 SQL 无法统计,wall 用于防火墙 日志 log4j
filters: stat,wall,log4j #导入了log4j
use-global-data-source-stat: true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

application.properties下配置

1
2
mybatis.type-aliases-package=com.sliu.shirospringboot.pojo
mybatis.mapper-locations=classpath:mapper/*.xml

新建pojo/User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sliu.shirospringboot.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String username;
private String password;
}

新建com.sliu.shirospringboot/mapper/UserMapper接口

1
2
3
4
5
6
7
8
9
10
11
package com.sliu.shirospringboot.mapper;

import com.sliu.shirospringboot.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String username);
}

新建resources/mapper/UserMappper.xml

1
2
3
4
5
6
7
8
9
<?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.sliu.shirospringboot.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="User">
select * from user where username =#{username}
</select>
</mapper>

新建service/userService接口和实现类service/UserServiceImpl

1
2
3
4
5
6
7
package com.sliu.shirospringboot.service;

import com.sliu.shirospringboot.pojo.User;

public interface UserService {
public User queryUserByName(String name);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.sliu.shirospringboot.service;

import com.sliu.shirospringboot.mapper.UserMapper;
import com.sliu.shirospringboot.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService{
@Autowired
UserMapper userMapper;

@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
}

修改自定义UserRealm下认证为数据库获取

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
49
50
51
package com.sliu.shirospringboot.config;

import com.sliu.shirospringboot.pojo.User;
import com.sliu.shirospringboot.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

//自定义的UserRealm,继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了授权======>doGetAuthorizationInfo");
return null;
}

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证======>doGetAuthenticationInfo");

//用户名,密码 从数据库中取
// String name = "root";
// String password = "123456";

UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// if(!userToken.getUsername().equals(name)){
// return null; //抛出异常 UnkonwnAccountException
// }

//密码认证,shiro实现
// return new SimpleAuthenticationInfo("",password,"");
//真实数据库获取账号密码并核验正确性
User user = userService.queryUserByName(userToken.getUsername());
//用户不存在
if(user==null){
return null;//UnknownAccountException
}

//密码认证,code无明文密码
return new SimpleAuthenticationInfo("",user.getPassword(),"");
}
}

Shiro请求授权

(1)设置权限

添加认证信息

这里需要留意的点在于以下两个授权的先后顺序,写反了导致所有用户都能访问到/user/add,调bug一小时

1
2
3
4
filterMap.put("/user/add", "perms[user:add]");

//设置user下的资源必须认证才能访问
filterMap.put("/user/*", "authc");
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.sliu.shirospringboot.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

//ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);

//添加shiro内置过滤器
/*
anon: 无需认证即可访问
authc: 必须认证才能访问
user: 必须配置记住我 功能才能用
perms: 拥有对某个资源的权限才能访问
role: 拥有某个角色权限才能访问
*/

Map<String, String> filterMap = new LinkedHashMap<>();
//设置授权,正常情况下,未授权跳转到未授权页面
//只有拥有user:add权限的用户才能访问/user/add资源
filterMap.put("/user/add", "perms[user:add]");

filterMap.put("/user/update", "perms[user:update]");

//设置user下的资源必须认证才能访问
filterMap.put("/user/*", "authc");

bean.setFilterChainDefinitionMap(filterMap);

//设置认证失败,登陆请求
bean.setLoginUrl("/toLogin");

//设置未授权跳转页面
bean.setUnauthorizedUrl("/unauthor");
return bean;
}

//DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(userRealm());
return securityManager;
}

//创建realm对象,需要自定义类
@Bean(name = "userRealm")
public UserRealm userRealm(){
return new UserRealm();
}
}

(2)授予用户权限

为了方便权限管理,修改数据库user表,添加一个perm字段用于存储用户权限信息,表结构如下

修改pojo/User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.sliu.shirospringboot.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String username;
private String password;
private String perm;
}

自定义UserRealm通过获取当前Subject附带的User对象获取当前用户权限perm信息

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.sliu.shirospringboot.config;

import com.sliu.shirospringboot.pojo.User;
import com.sliu.shirospringboot.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

//自定义的UserRealm,继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了授权======>doGetAuthorizationInfo");
//SimpleAuthorizationInfo
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

//获取当前登陆的对象Subject
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();

//获取当前用户权限
info.addStringPermission(currentUser.getPerm());

//返回info
return info;
}

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证======>doGetAuthenticationInfo");

//用户名,密码 从数据库中取
// String name = "root";
// String password = "123456";

UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// if(!userToken.getUsername().equals(name)){
// return null; //抛出异常 UnkonwnAccountException
// }

//密码认证,shiro实现
// return new SimpleAuthenticationInfo("",password,"");
//真实数据库获取账号密码并核验正确性
User user = userService.queryUserByName(userToken.getUsername());
//用户不存在
if(user==null){
return null;//UnknownAccountException
}

//支持加密
//密码认证,code无明文密码
return new SimpleAuthenticationInfo(user, user.getPassword(),"");
}
}

Shiro整合Thymeleaf

该部分功能实现对应权限仅显示对于功能页(即如果sliu仅拥有add权限,登陆主页后,应仅显示add操作按钮,而隐藏update操作按钮)

pom.xml中导入shiro-thymeleaf整合依赖包

1
2
3
4
5
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>

ShiroConfig中进行配置整合

1
2
3
4
5
//整合ShiroDialect 用来整合Shiro Thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}

!!!以下为部分前端知识

修改主页html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p>
<a th:href="@{/toLogin}">登陆</a>
</p>
<p th:text="${msg}"></p>
<hr>
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>

<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">update</a>
</div>
</body>
</html>

实现效果如下

当前在登陆上存在的问题是,登陆成功后仍然显示登陆按钮。通过在登陆认证时设置当前用户的Session,前端判断Session为空时才显示登陆按钮

自定义UserRealm类下添加认证时设置Session

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
 //认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证======>doGetAuthenticationInfo");

//用户名,密码 从数据库中取
// String name = "root";
// String password = "123456";

UsernamePasswordToken userToken = (UsernamePasswordToken) token;
// if(!userToken.getUsername().equals(name)){
// return null; //抛出异常 UnkonwnAccountException
// }

//密码认证,shiro实现
// return new SimpleAuthenticationInfo("",password,"");
//真实数据库获取账号密码并核验正确性
User user = userService.queryUserByName(userToken.getUsername());
//用户不存在
if(user==null){
return null;//UnknownAccountException
}

Subject currentSubject = SecurityUtils.getSubject();
Session session = currentSubject.getSession();
session.setAttribute("loginUser", user);

//支持加密
//密码认证,code无明文密码
return new SimpleAuthenticationInfo(user, user.getPassword(),"");
}

index.html下添加判断session是否为空

1
2
3
<div th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}">登陆</a>
</div>

成功登陆后不再显示登陆按钮

总结

本part中整合了常用安全框架SpringSecurity和Shiro,介绍了两种框架下如何实现用户的授权和认证

认证授权搞定√