简述

程序的访问安全性一直是开发的重要环节,JavaEE开源世界中有两大常用的安全框架:Apache Shiro 和 Spring Security。前者结构简单容易上手,后者结构复杂功能强大,多年来各有大量的支持者。 Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。而且Shiro的API也是非常简单。

四大核心功能


验证(Authentication)、授权(Authorization)、会话管理(Session Management)和加密管理(Cryptography)。

Authentication:身份认证/登录,验证用户是不是拥有相应的身份;

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support:Web支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去; Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

外部结构(应用程序角度)

可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;

(1)Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给

(2)SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

(3)Realm:安全域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操可以把Realm看成DataSource,即安全数据源。

内部架构

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

SessionManager:如果写过Servlet就应该知道Session的概念,Session需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;Shiro抽象了一个自己的Session来管理主体与应用之间交互的数据;

SessionDAO:数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

身份验证

身份验证,即在应用中谁能证明他就是他本人。一般提供如他们的身份ID一些标识信息来表明他就是他本人,如提供身份证,用户名/密码来证明。 在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份:

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。

credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。 最常见的principals和credentials组合就是用户名/密码了。

SecurityUtils:是一个抽象的工具类,提供了SecurityManager实例的保存和获取方法,以及创建Subject的方法。

UsernamePasswordToken:是一个简单的包含username及password即用户名及密码的登录验证用token,这个类同时继承了HostAuthenticationToken及RememberMeAuthenticationToken,主要包含用户名,密码,是否记住token以及验证来源的host主机地址。

核心过滤器

配置缩写 过滤器
anon AnonymousFilter 指定url可以匿名访问
authc FormAuthenticationFilter 指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录
authcBasic BasicHttpAuthenticationFilter 指定url需要basic登录
logout LogoutFilter 登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreation noSessionCreation 禁止创建会话
perms PermissionsAuthorizationFilter 需要指定权限才能访问
rest HttpMethodPermissionFilter 将http请求方法转化成相应的动词来构造一个权限字符串
roles RolesAuthorizationFilter 需要指定的角色才能访问
ssl SslFilter 需要https请求才能访问
user UserFilter 需要已登录或“记住我”的用户才能访问

网页简易demo

两个用户,一个张三(zhangsan)一个管理员(admin),分别有不同的权限

导包

1
2
3
4
5
6
<!-- shiro 整合spring boot web项目 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.1</version>
</dependency>

继承身份验证安全域

自定义安全域继承身份验证安全域
有两个作用,
1.获取身份验证信息(账号密码) 并且 验证用户信息
2.权限分配

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
//安全域继承抽象类后成为 身份验证安全域
@Component
public class MyRealm extends AuthorizingRealm {

// 1.获取身份验证信息 并且 验证用户信息
// subject.login(token); 后执行这里
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
UsernamePasswordToken token= (UsernamePasswordToken)authenticationToken;

//获取用户在浏览器中输入的账号
String username=token.getUsername();

//认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
String dbusername=username;
if(!"admin".equals(dbusername)&&!"zhangsan".equals(dbusername)){//判断用户账号是否正确
throw new UnknownAccountException("账号错误");
}

//定义一个密码这个密码应该来自数据库中
String dbpassword="123456";

//使用MD5哈希加密,不加盐,加密1次
Object objectPwd=new SimpleHash("MD5",dbpassword,"",1);

//认证密码是否正确
return new SimpleAuthenticationInfo(dbusername,objectPwd.toString(),this.getName());
}


// 2.权限分配
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//从Shiro中获取用户名
Object username=principalCollection.getPrimaryPrincipal();

//创建一个SimpleAuthorizationInfo类的对象,利用这个对象需要设置当前用户的权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();

//创建角色信息的集合
Set<String> roles=new HashSet<String>();

//这里应该根据账号到数据库中获取用户的所对应的所有角色信息并初始化到roles集合中
if("admin".equals(username)){
roles.add("admins");
roles.add("users");
}else if ("zhangsan".equals(username)){
roles.add("users");
}

// 权限集合
Set<String>psermission=new HashSet<String>();
if("admin".equals(username)){
psermission.add("admin:add");
}
// 设置角色信息
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(psermission);
return simpleAuthorizationInfo;
}


}

自定义安全管理器ShiroConfig

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
@Configuration //配置类
public class ShiroConfig {

@Bean //创建自定义安全域对象,方法名需要与安全管理器的入参一致
public MyRealm realm() {
// 新建自定义域
return new MyRealm();
}

//安全管理器,返回值要与权限拦截过滤器入参一致
@Bean
public DefaultWebSecurityManager securityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
//设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}


//配置权限拦截过滤器
@Bean //配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
//创建过滤器工厂对象,负责执行过滤器
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

//配置登录路径
shiroFilterFactoryBean.setLoginUrl("/");

//配置登录成功之后重定向的url
shiroFilterFactoryBean.setSuccessUrl("/success");

//配置权限不够,跳转的url
shiroFilterFactoryBean.setUnauthorizedUrl("/noPermission");

//定义过滤器集合
Map<String,String> map=new HashMap<>();
//登录验证允许匿名访问 anon是核心过滤器之一
map.put("/login","anon");

//设置过滤器链
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}


//开启Shiro的注解例如(@RequiresRoles @RequiresUser @RequiresPermissions)需要借助SpringAOP来扫描这些注解
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}


// 开启AOP的注解支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}

Controller

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

@Controller
public class UserController {
@RequestMapping("/")
public String index(){
return "login";
}

@RequestMapping("/login") //登录验证逻辑,类似dologin
public String login(String username, String password, Model model){
//获取主体Subject
Subject subject= SecurityUtils.getSubject();

//判断是否验证
//if(!subject.isAuthenticated())
{
//用户名密码票据对象
UsernamePasswordToken token=new UsernamePasswordToken();
token.setUsername(username);
token.setPassword(password.toCharArray());

//登录
try {
// 这里开始就转 身份验证安全域了
subject.login(token);
}
catch (UnknownAccountException ex){
model.addAttribute("errorMsg",ex.getMessage());
return "login";
}
catch (LockedAccountException ex){
model.addAttribute("errorMsg",ex.getMessage());
return "login";
}
catch (IncorrectCredentialsException ex){
model.addAttribute("errorMsg","密码不正确");
return "login";
}
}
return "redirect:/success";
}


@RequestMapping("/success")
public String success(){
return "success";
}


@RequestMapping("/noPermission")
public String noPermission(){
return "noPermission";
}

@RequestMapping("/user/test")
@RequiresRoles("users") //普通用户角色才能访问
@ResponseBody
public String userTest(){
return "这是普通用户请求";
}

@RequestMapping("/admin/test")
@RequiresRoles("admins") //管理员角色才能访问
@ResponseBody
public String adminTest(){
return "这是管理员的adminTest请求";
}

@RequestMapping("/admin/add")
@RequiresPermissions("admin:add")//权限 admin:add
@ResponseBody
public String adminAdd(){
return "这是管理员adminAdd请求";
}

@ExceptionHandler(value = {Exception.class})
public String myError(Throwable throwable){
//获取异常的类型,应该根据不同的异常类型进入到不通的页面显示不同提示信息
System.out.println(throwable.getClass());
System.out.println("---------------------------------");
return "nopermission";
}
}

定义页面

login.html

1
2
3
4
5
6
<form action="login" method="post">
账号<input type="text" name="username"><br>
密码<input type="text" name="password" id="password"><br>
<input type="submit" value="登录" id="loginBut">
</form>
<span style="color: red" th:text="${errorMessage}"></span>

nopermission.html

1
<h1>没有权限请联系管理员</h1>

success.html

1
<h1>登录成功</h1>

张三能够访问,/user/test
管理员能够访问/admin/add,/admin/test,/user/test

大致流程

shiro 运行大致流程
Shiro配置类(安全域、安全管理器、权限拦截过滤器、注解以及AOP支持)

身份认证
用户访问Controller后触发subject.login
触发后进入自定义安全域的身份验证(安全域需要继承AuthorizingRealm),一顿操作后成功则返回认证信息authenticationInfo,否则不返回

授权
每个Controller都有对应的授权页面

在安全区域授权

1
2
3
4
5
6
7
8
//获取用户名
String username=principalCollection.getPrimaryPrincipal().toString();
//创建授权对象
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
// 赋给授权 对象角色
authorizationInfo.addRole(角色);
// 添加权限
authorizationInfo.addStringPermission(权限);

Controller页面的每个对应的权限与角色
注解可以放在类上或者方法上

1
2
3
4
5
6
@RequiresPermissions("org") // 只有org权限的才能进入此页面
@RequestMapping("/index")
public String index()
{
return "org/index";
}
1
2
3
4
5
@RequiresRoles("user") // 只有user角色的才可以进入
@RequestMapping("/user-list")
public String userlist(Model model ){
return "security/user-list";
}

主要配置

shiro的主要配置

配置类

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

@Configuration //配置类
public class ShiroConfiguration {
@Bean //创建自定义安全域对象,方法名需要与安全管理器的入参一致
public MyRealm realm() {
// 新建自定义域
MyRealm myRealm = new MyRealm();
// 这里可以用自带的加密 设置MD5加密
HashedCredentialsMatcher credentialMatcher = new HashedCredentialsMatcher();
credentialMatcher.setHashAlgorithmName("MD5");
credentialMatcher.setHashIterations(1024);
myRealm.setCredentialsMatcher(credentialMatcher);
return myRealm;
}

//安全管理器,返回值要与权限拦截过滤器入参一致
@Bean
public DefaultWebSecurityManager securityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager=new DefaultWebSecurityManager();
//设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}

//配置权限拦截过滤器
@Bean //配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
//访问控制
chain.addPathDefinition("/login", "anon");//可以匿名访问
chain.addPathDefinition("/deny", "anon");//可以匿名访问
chain.addPathDefinition("/menus", "anon");//可以匿名访问
chain.addPathDefinition("/logout", "logout");//可以匿名访问
//其它路径均需要登录
chain.addPathDefinition("/**/*", "authc");
return chain;
}

//开启Shiro的注解例如(@RequiresRoles @RequiresUser @RequiresPermissions)需要借助SpringAOP来扫描这些注解
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}


// 开启AOP的注解支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}

访问控制过滤器还能在配置文件里面配置

1
2
shiro.loginUrl=/login
shiro.successUrl=/index

授权与身份验证

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
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;

@Override //授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取用户名
String username=principalCollection.getPrimaryPrincipal().toString();
System.out.println("获取用户名" + username);

//根据用户名查找出对象
User user = userService.findByUsername(username);

//创建授权对象
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
System.out.println("创建授权对象"+ authorizationInfo);

for (Role role : user.getRoles()) {
// 赋给授权对象角色
authorizationInfo.addRole(role.getName());
}
//获取当前用户所有的权限编码
List<Permission> permissionList=permissionService.findPermissionsByUsername(username);

for (Permission permission : permissionList) {
//添加父权限
authorizationInfo.addStringPermission(permission.getCode());
for (Permission child : permission.getChildren()) {
//添加子权限
authorizationInfo.addStringPermission(child.getCode());
}
}

return authorizationInfo;
}

@Override //身份验证---控制前执行subject.login调用此方法
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

//获取用户输入的身份标识
Object principal = authenticationToken.getPrincipal();
// doLogin传入的username,但类型是Object
System.out.println("获取用户输入的身份标识" + principal);

// 是否验证过了
if (principal != null){

// 获取用户名,要转换为string
String username=principal.toString();
User user = userService.findByUsername(username);
// 数据库查询不到此用户
if (user==null){
throw new UnknownAccountException("用户名不正确");
}else if (user.getStatus() == 0){
throw new LockedAccountException("账户已经锁定");
}else {
// 认证通过后,加盐
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username,
user.getPassword(),
ByteSource.Util.bytes(user.getUsername()), //shiro自带方法加盐 , 根据用户名
this.getName());
return authenticationInfo;
}
}
return null;
}
}