shiro介绍
官网: Apache Shiro | Simple. Java. Security.
Shiro 是 Java 的一个安全(权限)框架。
shiro功能
Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存等。
-
Authentication: 身份认证/登录,验证用户是不是拥有相应的身份
-
Authorization: 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
...... (其他功能略,自己百度)
shiro核心组件
下述为实现shiro较为核心的几个组件
ShiroFilterFactory是整个Shiro的入口点
简而言之:
-
对于
Realms
,我们只需要继承 抽象类AuthorizingRealm
即可发现,我们需要重写两个方法,分别是用于实现认证功能的doGetAuthenticationInfo
方法 和用于实现授权的doGetAuthorizationInfo
方法。
-
SecurityManager
属于一个中间安全管理器,通过它可实现各种安全服务 -
ShiroFilterFactoryBean
则属于一个过滤器工厂,我们可以在这里定义哪些接口需要认证和授权
而对于shiro的快速入门,其实就是对以上的,SecurityManager、Realms、ShiroFilterFactoryBean三个类的重写与实现。
使用
我这个只演示认证功能的实现,其他功能,如授权,需要你自行百度。
创建一个普通的SpringBoot工程,只需要选择web依赖即可。
1、引入依赖
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2、编写用于测试的基本接口
VO类,用于接收用户名和密码
@Data
public class UserLoginVo {
private String userName;
private String passWord;
}
登录接口和另一个普通访问的接口
@RestController
public class UserController {
/**
* 登录
*
* @param loginVo 用户名和密码
* @return {@link String}
*/
@PostMapping("/login")
public String login(@RequestBody UserLoginVo loginVo) {
System.out.println(loginVo);
return "登录成功";
}
@GetMapping("/hello")
public String hello() {
System.out.println("hello");
return "打招呼";
}
}
用接口测试两个接口,都可使用
3、实现shiro核心组件
Realms组件的实现
@Component
public class AccountRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权");
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证");
// 获取登录的用户名
String username = (String) token.getPrincipal();
// 判断改用户是否存在
if (!"hsx".equals(username)) {
throw new UnknownAccountException("账户不存在!");
}
// 验证密码
return new SimpleAuthenticationInfo(username, "123", getName());
}
}
在此处,我账号和密码分别写死为 "hsx" 和 "123"。在实际开发中,这部分的账号和密码的验证,应该是来着查询数据库的。
ShiroFilterFactoryBean和SecurityManager组件的实现
在这里,我直接演示使用注解的方法来实现安全管理。
@Configuration
public class ShiroConfig {
//DefaultWebSecurityManager 安全管理器
@Bean
public DefaultWebSecurityManager securityManager(AccountRealm accountRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联 我们自定义的AccountRealm
securityManager.setRealm(accountRealm);
return securityManager;
}
//shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(securityManager);
return bean;
}
//开启注解代理(默认好像已经开启,可以不要)
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
4、测试
在配置了shiro的三个核心组件,SecurityManager、Realms、ShiroFilterFactoryBean。而且,重写了Realms中的认证方法。那我们就可以按照我们定义的规则进行认证了。
只要我们在Controller接口上加上@RequiresAuthentication注解,那么该接口就需要认证之后才能访问。
@GetMapping("/hello")
@RequiresAuthentication
public String hello() {
System.out.println("hello");
return "打招呼";
}
重启服务,调用 'hello' 接口。然后发现,系统报错,提示我们权限不足。无法访问该接口。
若要访问改接口,那么我们需要进行认证
在shiro中,只有调用SecurityUtils.getSubject()
中的login
,才可反问我们上面定义的doGetAuthenticationInfo方法。
所以,我们在login
接口调用我们上述所说的方法,将前端传过来的账号和密码都传进去
/**
* 登录
*
* @param loginVo 用户名和密码
* @return {@link String}
*/
@PostMapping("/login")
public String login(@RequestBody UserLoginVo loginVo) {
SecurityUtils.getSubject().login(
new UsernamePasswordToken(loginVo.getUserName(), loginVo.getPassWord()));
System.out.println(loginVo);
return "登录成功";
}
然后通过接口测试工具来调用/login
接口,通过debug发现,的确进入了我们所定义的方法
此时,我们再次调用/hello
接口,然后可以发现,可以返回我们需要的数据
5、优化
在上述,当未认证时,访问接口,前端则会报 500 错误。此时,我们可以定义个全局异常捕获,让报错更加的优雅。
package com.hsx.utils;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestControllerAdvice
public class BaseExceptionHandler {
/**
* 处理未授权的异常
*/
@ExceptionHandler(value = AuthorizationException.class)
public String returnErrorMsg(HttpServletRequest request, HttpServletResponse response, AuthorizationException exception) {
return "您没有权限";
}
/**
* 处理未登录的异常
*/
@ExceptionHandler(value = UnauthenticatedException.class)
public String returnUnLoginErrorMsg(HttpServletRequest request, HttpServletResponse response, UnauthenticatedException exception) {
return "您还未登录";
}
/**
* 处理运行时异常
*/
@ExceptionHandler(value = Exception.class)
public String returnRunningException(Exception e) {
System.out.println(e.getMessage());
return "系统错误";
}
}
此时,再次访问没有权限的接口。
总结
上述代码,可以先根据我的步骤来实现,然后对比oj系统中相应部分的实现。其实原理都是一样的。不一样的地方,比如在认证方法doGetAuthenticationInfo
那里,我为了方便,就是将账号密码写死,而在oj系统中,是进行了JWT反解析获取到用户ID,然后去数据库查询用户是否存在,最后才进行密码的比较。