基本上,在所有的开发的系统中,都必须做认证(authentication)和授权(authorization),以保证系统的安全性。
认证 (authentication) 和授权 (authorization) 的区别:
以坐火车举例子:
所以简单来说:认证解决“你是谁”的问题,授权解决“你能做什么”的问题。
而在 Java 生态中,目前有 Spring Securit 和 Apache Shiro 两个安全框架,可以完成认证和授权的功能。
Apache Shiro ,其官方对自己介绍如下:
FROM 《Apache Shiro 官网》
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,它可以提供身份验证、授权、加密和会话管理的功能。
With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications. 通过 Shiro 易于理解的 API ,你可以快速、轻松地保护任何应用程序 —— 从最小的移动端应用程序到大型的的 Web 和企业级应用程序。
使用 Shiro 进行快速的入门,实现一个最小化的使用示例。
创建一个maven项目,并在pom.xml文件中,引入相关依赖。
<!-- spring-boot启动器 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 实现对 Shiro 的自动化配置 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
</dependencies>
在 com.xsh.config 包下,创建 ShiroConfig 抽象类,实现 Shiro 的自定义配置。代码如下:
// ShiroConfig.java
@Configuration
public class ShiroConfig {
@Bean
public Realm realm() { /**省略代码**/ }
@Bean
public DefaultWebSecurityManager securityManager() { /**省略代码**/ }
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() { /**省略代码**/ }
}
一共有三个 Bean 的配置,逐个分析:
先来看看 Realm 的定义。
[《Apache Shiro 1.2.x 参考手册 —— Realms》](https://waylau.com/apache-shiro-1.2.x-reference/II. Core 核心/7. Realms.html)
Realm 是可以访问程序特定的安全数据如用户、角色、权限等的一个组件。Realm 会将这些程序特定的安全数据转换成一种 Shiro 可以理解的形式,Shiro 就可以依次提供容易理解的 [Subject](https://waylau.com/apache-shiro-1.2.x-reference/IV. Auxiliary Support 辅助支持/14. Custom Subjects 自定义 Subject.html) 程序API而不管有多少数据源或者程序中你的数据如何组织。
Realm 通常和数据源是一对一的对应关系,如关系数据库,LDAP 目录,文件系统,或其他类似资源。因此,Realm 接口的实现使用数据源特定的API 来展示授权数据(角色,权限等),如JDBC,文件IO,Hibernate 或JPA,或其他数据访问API。
Realm 实质上就是一个特定安全的 DAO
因为这些数据源大多通常存储身份验证数据(如密码的凭证)以及授权数据(如角色或权限),每个 Shiro Realm 能够执行身份验证和授权操作。
Realm 整体的类图如下:
// Realm.java
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
// AuthorizingRealm.java
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
AuthorizingRealm 同时实现了 Authorizer 接口,提供判断经过认证过的 Subject 是否具有指定的角色、权限的方法。
从Realm 整体的类图中可以看出,Shiro 提供了多种 AuthorizingRealm 的实现类,提供从不同的数据源获取数据。不过一般在项目中,我们会自定义实现 AuthorizingRealm ,从自己定义的表结构中读取用户、角色、权限等数据。虽然说,Shiro 提供了 JdbcRealm可以访问数据库,但是它的表结构是固定的,所说才要自定义定义实现 AuthorizingRealm 。
本示例中,在 realm()方法,我们创建了 SimpleAccountRealm Bean 对象。代码如下:
// ShiroConfig.java
@Bean
public Realm realm() {
// 创建 SimpleAccountRealm 对象
SimpleAccountRealm realm = new SimpleAccountRealm();
// 添加两个用户。参数对应 username、password、roles 。
realm.addAccount("admin", "123456", "ADMIN");
realm.addAccount("user", "123456", "USER");
return realm;
}
先看看 SecurityManager 的定义。
[《Apache Shiro 1.2.x 参考手册 —— Session Management》](https://waylau.com/apache-shiro-1.2.x-reference/II. Core 核心/8. Session Management.html)
SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
本示例中,在 securityManager()
方法,我们创建了 DefaultWebSecurityManager Bean 对象。代码如下:
// ShiroConfig.java
@Bean
public DefaultWebSecurityManager securityManager() {
// 创建 DefaultWebSecurityManager 对象
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置其使用的 Realm
securityManager.setRealm(this.realm());
return securityManager;
}
通过 AbstractShiroFilter 过滤器,实现对请求的拦截,从而实现 Shiro 的功能。AbstractShiroFilter 整体的类图如下:
本示例中,在 #shiroFilterFactoryBean()
方法,我们创建了 ShiroFilterFactoryBean Bean 对象。代码如下:
// ShiroConfig.java
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
// <1> 创建 ShiroFilterFactoryBean 对象,用于创建 ShiroFilter 过滤器
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// <2> 设置 SecurityManager
filterFactoryBean.setSecurityManager(this.securityManager());
// <3> 设置 URL
filterFactoryBean.setLoginUrl("/login"); // 登陆 URL
filterFactoryBean.setSuccessUrl("/login_success"); // 登陆成功 URL
filterFactoryBean.setUnauthorizedUrl("/unauthorized"); // 无权限 URL
// <4> 设置 URL 的权限配置
filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());
return filterFactoryBean;
}
<4>
处,调用 setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap)
方法,设置 URL 的权限配置。在看 #filterChainDefinitionMap()
方法的具体 URL 的权限配置之前,我们先来了解下 Shiro 内置的过滤器们。在 Shiro DefaultFilter 枚举类中,枚举了这些过滤器,以及其配置名。整理表格如下:
比较常用的过滤器有:
anon
:AnonymousFilter :允许匿名访问,即无需登陆。GET loginUrl
登陆页面,则进行该请求,跳转到登陆页面。POST loginUrl
登陆请求,则基于请求表单的 username
、password
进行认证。认证通过后,默认重定向到 GET loginSuccessUrl
地址。logout
:LogoutFilter :拦截的 URL ,执行退出操作。退出完成后,重定向到 GET loginUrl
登陆页面。roles
:RolesAuthorizationFilter :拥有指定角色的用户可访问。perms
:PermissionsAuthorizationFilter :拥有指定权限的用户可以访问。定义 filterChainDefinitionMap()
方法的具体 URL 的权限配置。代码如下:
// ShiroConfig.java
private Map<String, String> filterChainDefinitionMap() {
Map<String, String> filterMap = new LinkedHashMap<>(); // 注意要使用有序的 LinkedHashMap ,顺序匹配
filterMap.put("/test/echo", "anon"); // 允许匿名访问
filterMap.put("/test/admin", "roles[ADMIN]"); // 需要 ADMIN 角色
filterMap.put("/test/user", "roles[USER]"); // 需要 USER 角色
filterMap.put("/logout", "logout"); // 退出
filterMap.put("/**", "authc"); // 默认剩余的 URL ,需要经过认证
return filterMap;
}
/test/echo
:设置为 anon
,允许匿名访问。/test/admin
和 /test/user
:我们设置为 roles[...]
,需要指定角色的用户可以访问。其中 ...
处为需要添加的角色名。/logout
:我们设置为 logout
,实现退出操作。/**
:剩余的 URL ,我们设置为 authc
,需要登陆的用户才可以访问。同时,对于 loginUrl
需要执行登陆相关的拦截规则,设置为不拦截允许匿名访问。另外,请求在 ShiroFilter 拦截之后,会根据该请求的情况,匹配到配置的内置的 Shiro Filter 们,逐个进行处理。也就是说,ShiroFilter 实际内部有一个由 内置的 Shiro Filter 组成的过滤器链。
在 com.xsh.controller包路径下,创建 SecurityController 类,提供登陆、登陆成功等接口。代码如下:
// SecurityController.java
@Controller
@RequestMapping("/")
public class SecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
@GetMapping("/login")
public String loginPage() {
return "login.html";
}
@ResponseBody
@PostMapping("/login")
public String login(HttpServletRequest request) {
// 判断是否已经登陆
Subject subject = SecurityUtils.getSubject();
if (subject.getPrincipal() != null) {
return "你已经登陆账号:" + subject.getPrincipal();
}
// 获得登陆失败的原因
String shiroLoginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
// 翻译成人类看的懂的提示
String msg = "";
if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
msg = "账号不存在";
} else if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
msg = "密码不正确";
} else if (LockedAccountException.class.getName().equals(shiroLoginFailure)) {
msg = "账号被锁定";
} else if (ExpiredCredentialsException.class.getName().equals(shiroLoginFailure)) {
msg = "账号已过期";
} else {
msg = "未知";
logger.error("[login][未知登陆错误:{}]", shiroLoginFailure);
}
return "登陆失败,原因:" + msg;
}
@ResponseBody
@GetMapping("/login_success")
public String loginSuccess() {
return "登陆成功";
}
@ResponseBody
@GetMapping("/unauthorized")
public String unauthorized() {
return "你没有权限";
}
}
GET /login
地址,跳转登陆,返回 resources/static/login.html 静态页面。代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆页面</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"/> <br />
密码:<input type="password" name="password"/> <br />
<input type="submit" value="登录"/>
</form>
</body>
</html>
一个简单的登陆的表单,POST
提交登陆请求到 /login
地址上。
对于登陆请求,会被我们配置的 Shiro FormAuthenticationFilter 过滤器进行拦截,进行用户的身份认证。整个过程如下:
username
、password
参数,创建 UsernamePasswordToken对象。login(Subject subject, AuthenticationToken authenticationToken)
方法,执行登陆操作,进行“身份验证”(认证)。getAuthenticationInfo(AuthenticationToken token)
方法,进行认证。此时,根据认证的是否成功,会有不同的处理:
GET loginSuccess
地址上。attributes
中,后续该请求会继续请求到 POST login
地址上。这样,在 POST loginUrl
地址上,我们可以从 attributes
中获取到失败的原因,提示给用户。所以,POST loginUrl
的目的,实际是为了处理认证失败的情况。
对于已经登陆成功的用户,如果再次请求 POST loginUrl
地址,依然会请求转发到POST loginUrl
上。此处,我们是提供用户已经的登陆的提示。 如果希望重新进行一次登陆的逻辑,那么就需要重写 FormAuthenticationFilter 过滤器。
获得登陆失败的原因,从请求的 attributes
中,获取 FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME
对应的值,即登陆失败的原因。从代码中可以看出,失败原因为异常的全类名,需要进行翻译成人类可读的提示。
GET login_success
地址,登陆成功响应。
如果是 AJAX 请求的情况下,我们可以返回 JSON 字符串。例如说,用户、角色、权限等等信息。
如果非 AJAX 请求的情况下,重定向到登陆成功的页面。例如说,管理后台的 HOME 页面。
GET unauthorized
地址,未授权响应。
在 com.xsh.controller
包路径下,创建 TestController类,提供测试 API 接口。代码如下:
// TestController.java
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/demo")
public String demo() {
return "示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@GetMapping("/user")
public String user() {
return "我是普通用户";
}
}
/test/demo
接口,直接访问,无需登陆。/test/home
接口,无法直接访问,需要进行登陆。/test/admin
接口,需要登陆「admin/123456」用户,因为需要 ADMIN 角色。/test/user
接口,需要登陆「user/123456」用户,因为需要 USER 角色。登陆「user/user」用户后,去访问 /test/admin
接口,会返回无权限的提示~
springBoot启动类,在com.xsh目录下新建Application.java,与controller目录和config目录平级
// Application.java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
启动后,端口默认8080,使用「admin/123456」登录后出现spring图标则登录成功
在 Shiro 中,提供了如下五个注解,可以直接添加在 SpringMVC 的 URL 对应的方法上,实现权限配置。
@RequiresGuest
注解,和 anon
等价。
@RequiresAuthentication
注解,和 authc
等价。
@RequiresUser
注解,和 user
等价,要求必须登陆。
@RequiresRoles
注解,和 roles
等价。
其中@RequiresRoles使用示例如下:
// 属于 NORMAL 角色
@RequiresRoles("NORMAL")
// 要同时拥有 ADMIN 和 NORMAL 角色
@RequiresRoles({"ADMIN", "NORMAL"})
// 同时拥有 ADMIN 和 NORMAL 两个角色才允许访问
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.AND)
// 拥有 ADMIN 或 NORMAL 任一角色即可
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)
如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice
+ @ExceptionHandler
注解,实现全局异常的处理。
@RequiresPermissions
注解,和 perms
等价。使用示例如下:
// 拥有 user:add 权限
@RequiresPermissions("user:add")
// 要同时拥有 user:add 和 user:update 权限
@RequiresPermissions({"user:add", "user:update"})
// 同时拥有 user:add 和 user:update 两个权限才允许访问
@RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.AND)
// 拥有 user:add 和 user:update 任一权限即可
@RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)
如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice
+ @ExceptionHandler
注解,实现全局异常的处理。
在TestController的基础上,增加 Shiro 注解的使用。
在 com.xsh.controller
包路径下,创建 DemoController 类,提供示例 API 接口。代码如下:
// DemoController.java
@RestController
@RequestMapping("/demo")
public class DemoController {
@RequiresGuest
@GetMapping("/echo")
public String demo() {
return "示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@RequiresRoles("ADMIN")
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@RequiresRoles("USER")
@GetMapping("/user")
public String user() {
return "我是普通用户";
}
}
<shiro:guest>
游客访问 <a href = "login.jsp"></a>
</shiro:guest>
user 标签:用户已经通过认证或者记住我 登录后显示响应的内容
<shiro:user>
欢迎[<shiro:principal/>]登录 <a href = "logout">退出</a>
</shiro:user>
authenticated标签:用户身份验证通过,即 Subjec.login 登录成功 不是记住我登录的
<shiro:authenticted>
用户[<shiro:principal/>] 已身份验证通过
</shiro:authenticted>
notAuthenticated标签:用户未进行身份验证,即没有调用Subject.login进行登录,包括"记住我"也属于未进行身份验证
<shiro:notAuthenticated>
未身份验证(包括"记住我")
</shiro:notAuthenticated>
principal 标签:显示用户身份信息,默认调用
Subjec.getPrincipal()获取,即Primary Principal
<shiro:principal property = "username"/>
hasRole标签:如果当前Subject有角色将显示body体内的内容
<shiro:hashRole name = "admin">
用户[<shiro:principal/>]拥有角色admin
</shiro:hashRole>
hasAnyRoles标签:如果Subject有任意一个角色(或的关系)将显示body体里的内容
<shiro:hasAnyRoles name = "admin,user">
用户[<shiro:pricipal/>]拥有角色admin 或者 user
</shiro:hasAnyRoles>
lacksRole:如果当前 Subjec没有角色将显示body体内的内容
<shiro:lacksRole name = "admin">
用户[<shiro:pricipal/>]没有角色admin
</shiro:lacksRole>
hashPermission:如果当前Subject有权限将显示body体内容
<shiro:hashPermission name = "user:create">
用户[<shiro:pricipal/>] 拥有权限user:create
</shiro:hashPermission>
lacksPermission:如果当前Subject没有权限将显示body体内容
<shiro:lacksPermission name = "org:create">
用户[<shiro:pricipal/>] 没有权限org:create
</shiro:lacksPermission>
评论