1.概述
基本上,在所有的开发的系统中,都必须做认证(authentication)和授权(authorization),以保证系统的安全性。
简单来说:认证解决“你是谁”的问题,授权解决“你能做什么”的问题。
在Java生态中,目前两大安全框架是Spring Security和Apache Shiro,可以用来完成认证和授权的功能。
关于Shiro:
Apache Shiro 是一个功能强大且易于使用的 Java 安全框架,它可以提供身份验证、授权、加密和会话管理的功能。
通过 Shiro 易于理解的 API ,你可以快速、轻松地保护任何应用程序 —— 从最小的移动端应用程序到大型的的 Web 和企业级应用程序。
2.快速入门
2.1 引入依赖
<!--实现对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>
2.2 ShiroConfig
创建com.gavin.shiro.config.ShiroConfig配置类,用来实现Shiro的自定义配置,代码如下:
@Configuration
public class ShiroConfig {
@Bean
public Realm realm(){
}
@Bean
public DefaultWebSecurityManager securityManager(){
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
}
}
下面重点分析三个Bean的配置。
2.2.1 Realm
Realm是可以访问程序特定的安全数据如用户、角色、权限等的一个组件,Realm可以将这些程序特定的安全数据转换成一种Shiro可以理解的形式。简单说:Realm的职责就是进行身份认证和授权。
Realm整体的类图如下:
Realm接口,主要定义了认证的方法,代码如下:
public interface Realm {
String getName();
boolean supports(AuthenticationToken var1);
AuthenticationInfo getAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;
}
AuthorizingRealm抽象类,额外定义了授权方法,代码如下:
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection var1);
同时它又实现了Authorizer接口,提供判断经过认证过的Subject是否具有指定的角色、权限方法。
从Realm类图结构可以看出,Shiro提供了多种AuthorizingRealm的实现类,提供从不同的数据源获取数据,不过在一般的项目中,我们会自定义实现Authorizing Realm来从自己定义的表结构中读取用户、角色、权限等数据。虽然Shiro提供了JdbcRealm可以访问数据库,但是它的表结构是固定的,所以我们才选择自定义实现AuthorizingRealm。
在本示例中,在com.gavin.shiro.config.ShiroConfig#realm
方法中,我们创建了SimpleAccountRealm对象,代码如下:
@Bean
public Realm realm(){
//创建SimpleAccountRealm对象
SimpleAccountRealm realm = new SimpleAccountRealm();
//添加两个用户,参数分别是username、password、roles
realm.addAccount("admin","admin","ADMIN");
realm.addAccount("normal","normal","NORMAL");
return realm;
}
在该方法中,我们添加了两个用户,分别对应ADMIN和NORMAL角色。
2.2.2 SecurityManager
SecurityManager是Shiro架构的核心,配合内部安全组件共同组成安全伞。
在本示例中,在com.gavin.shiro.config.ShiroConfig#securityManager
方法中,我们创建了DefaultWebSecurityManager对象,代码如下:
@Bean
public DefaultWebSecurityManager securityManager(){
//创建DefaultWebSecurityManager对象
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置其使用的Realm
securityManager.setRealm(this.realm());
return securityManager;
}
2.2.3 ShiroFilter
通过AbstractShiroFilter过滤器,实现对请求的拦截,从而实现Shiro的功能,AbstractShiroFilter整体的类图如下:
在本示例中,在com.gavin.shiro.config.ShiroConfig#shiroFilterFactoryBean
方法中,我们创建了ShiroFilterFactoryBean对象,代码如下:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
//<1>创建ShiroFilterFactoryBean对象,用于创建ShiroFilter过滤器
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
//<2>设置其SecurityManager属性
filterFactoryBean.setSecurityManager(this.securityManager());
//<3>设置URL
//登录URL
filterFactoryBean.setLoginUrl("/login");
//登录成功URL
filterFactoryBean.setSuccessUrl("/login_success");
//无权限URL,在请求校验权限不通过时,会重定向到该URL上
filterFactoryBean.setUnauthorizedUrl("/unauthorized");
//<4>设置URL的权限配置
filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());
return filterFactoryBean;
}
在介绍filterChainDefinitionMap
方法的具体URL的权限配置之前,我们先来了解一下Shiro内置的过滤器,在DefaultFilter
枚举类中,枚举了这些过滤器,以及其配置名:
比较常用的过滤器有:
anon:AnonymousFilter,允许匿名访问,无需登录;
authc:FormAuthenticationFilter,需要经过认证的用户才可以访问,如果是匿名用户,则根据URL不同,会有不同的处理:
1、如果拦截的URL是GET loginUrl登录页面,则进行该请求,跳转到登录页面;
2、如果拦截的URL是POST loginUrl登录请求,则基于请求表单的username、password进行认证,认证通过后,默认重定向到GET loginSuccessUrl地址;
3、如果拦截的URL是其他URL时,则记录该URL到Session中,在用户登录成功后,重定向到该URL上。logout:LogoutFilter,拦截的URL,执行退出登录,退出完成后,重定向到GET loginUrl登录页面。
roles:RolesAuthorizationFilter,拥有指定角色的用户可访问;
perms:PermissionsAuthorizationFilter,拥有指定权限的用户可以访问。
下面,让我们回过头来看看
#filterChainDefinitionMap()
方法的具体URL的权限配置,代码如下:
/** URL的权限配置*/
private Map<String, String> filterChainDefinitionMap() {
//使用有序的LinkedHashMap,用来实现顺序匹配
LinkedHashMap<String,String> filterMap = new LinkedHashMap<>();
//允许匿名访问
filterMap.put("/test/echo","anon");
//需要ADMIN角色才能访问
filterMap.put("/test/admin","roles[ADMIN]");
//需要NORMAL角色才能访问
filterMap.put("/test/normal","roles[NORMAL]");
//注销登录
filterMap.put("/logout","logout");
//默认剩余的URL,都要经过认证后才能访问
filterMap.put("/**","authc");
return filterMap;
}
这里补充一点,请求在ShiroFilter拦截之后,会根据该请求的情况,匹配到配置中内置的所有Shio Filter,逐个进行处理,也就是说Shiro Filter内部有一个由内置的Shiro Filter组成的过滤器链。
2.3 SecurityController
创建一个SecurityController类,用来提供登录,登录成功等接口,代码如下:
com.gavin.shiro.controller.SecurityController
@Controller
@RequestMapping("/")
public class SecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
@GetMapping("/login")
public String loginPage(){
}
@ResponseBody
@PostMapping("/login")
public String login(HttpServletRequest request){
}
@ResponseBody
@GetMapping("/login_success")
public String loginSuccess(){
}
@ResponseBody
@GetMapping("/unauthorized")
public String unauthorized(){
}
}
2.3.1 登录页面
GET /login地址,来到登录页面,代码如下:
@GetMapping("/login")
public String loginPage(){
return "login.html";
}
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地址。
2.3.2 登录请求
对于登录请求,会被我们配置的FormAuthenticationFilter
过滤器进行拦截,进行用户的身份认证,过程如下:
FormAuthenticationFilter解析请求的
username
、password
参数,创建UsernamePasswordToken对象;然后,调用SecurityManager的
login(Subject subject, AuthenticationToken authenticationToken)
方法,执行登录操作,进行“身份验证”(认证);在这内部其实是调用Realm的
个体Authentication Info(AuthenticationToken token)
方法进行认证,此时根据认证是否成功,会有不同的处理方式:
1、如果认证通过,则FormAuthenticationFilter会将请求重定向到Get loginSuccess地址上;
2、如果认证失败,则会将认证失败的原因设置到请求的attributes中,后续该请求会继续请求到POST login地址上,这样,在POST loginUrl地址上,我们可以从attributes中获取到失败的原因提示给用户。所以,POST loginUrl的目的实际上是为了处理认证失败的情况,其实现的代码如下:
@ResponseBody
@PostMapping("/login")
public String login(HttpServletRequest request){
// <1> 判断是否已经登录
Subject subject = SecurityUtils.getSubject();
if(subject.getPrincipal() != null){
return "你已经登录,无需重复登录"+subject.getPrincipal();
}
// <2> 获得登录失败的原因
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;
}
2.3.3 登录成功
GET login_success地址,登录成功后响应,代码如下:
@ResponseBody
@GetMapping("/login_success")
public String loginSuccess(){
return "恭喜你,登录成功!!";
}
如果是AJAX请求,我们可以返回json数据;
如果是非AJAX请求,我们可以重定向到登录成功的页面,比如管理后台的home页面。
2.3.4 未授权
GET unauthorized地址,未授权响应,代码如下:
@ResponseBody
@GetMapping("/unauthorized")
public String unauthorized(){
return "对不起,您没有权限进行操作";
}
如果是AJAX请求,我们可以返回json数据;
如果是非AJAX请求,我们可以重定向到登录页面。
2.4 TestController
测试API接口:
@Controller
@RequestMapping("/test")
public class TestController {
@GetMapping("/demo")
public String demo(){
return "示例返回";
}
@GetMapping("/home")
public String home(){
return "首页";
}
@GetMapping("/admin")
public String admin(){
return "我是管理员";
}
@GetMapping("/normal")
public String normal(){
return "我是普通用户";
}
}
- 对于 /test/demo 接口,直接访问,无需登陆。
- 对于 /test/home 接口,无法直接访问,需要进行登陆。
- 对于 /test/admin 接口,需要登陆「admin/admin」用户,因为需要 ADMIN 角色。
- 对于 /test/normal 接口,需要登陆「user/user」用户,因为需要 USER 角色。
3.Shiro注解
在Shiro中,提供了如下5个注解,可以直接添加在SpringMVC的URL对应的方法上,实现权限配置:
3.1 @RequiresGuest
等同于anon
3.2 @RequiresAuthentication
等同于authc
3.3 @RequiresUser
等同于user
,要求必须登录
3.4 @RequiresRoles
和roles等价,代码如下:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {
String[] value();
//当有多个角色时,AND表示要拥有全部角色,OR表示拥有任意角色即可
Logical logical() default Logical.AND;
}
示例代码如下:
// 属于 NORMAL 角色
@RequiresRoles("NORMAL")
// 要同时拥有 ADMIN 和 NORMAL 角色
@RequiresRoles({"ADMIN", "NORMAL"})
// 拥有 ADMIN 或 NORMAL 任一角色即可(OR)
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)
如果验证角色不通过,就会抛出AuthorizationException异常,此时我们可以基于Spring MVC提供的@RestControllerAdvice+@ExceptionHandler注解,实现全局异常的处理。
不了解可以看看《芋道 Spring Boot SpringMVC 入门》的「5. 全局异常处理」小节。
3.5 @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.OR)
同样如果验证权限不通过,则会抛出AuthorizationException异常。
3.6 使用示例
新建DemoController类,提供示例API接口,代码如下:
@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("NORMAL")
@GetMapping("/normal")
public String normal(){
return "我是普通用户";
}
}
参考文献:
https://mp.weixin.qq.com/s/NmwqOM5rSDlmvs-Bi4P2Ag
github源码地址:点这里
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!