何为Shiro
Apache Shiro 是一个开源、轻量级的 Java 安全框架,它提供身份验证、授权、密码管理以及会话管理等功能。相对于 Spring Security,Shiro 框架更加直观、易用,同时也能提供健壮的安全性。
在传统的 SSM 框架中,手动整合 Shiro 的配置步骤还是比较多的,针对 Spring Boot,Shiro 官方提供了 shiro-spring-boot-web-starter 用来简化 Shiro 在 Spring Boot 中的配置。也就是可以引入以下依赖。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
为什么要使用Apache Shiro?
自2003年以来,框架格局发生了很大变化,因此今天仍然有充分的理由使用Shiro。实际上有很多原因。Apache Shiro特点如下:
- 易于使用 :易于使用是该项目的最终目标。应用程序安全性可能非常令人困惑和沮丧,并被视为“必要的邪恶”。如果您使它易于使用,以使新手程序员可以开始使用它,那么就不必再痛苦了。
- 全面 :没有其他安全框架可以达到Apache Shiro宣称的广度,它可以为你的安全需求提供“一站式”服务。
- 灵活 :Apache Shiro可以在任何应用程序环境中工作。Shiro也不要求任何规范,甚至没有很多依赖性。
- 具有Web功能 :Apache Shiro具有出色的Web应用程序支持,允许您基于应用程序URL和Web协议(例如REST)创建灵活的安全策略。
示例程序
application.properties 中配置
Shrio基本的配置信息如下,有些使用默认就好了。
# 配置登录地址,默认为"login.jsp"
shiro.loginUrl=/login
# 配置登录成功地址,默认为"/"
shiro.successUrl=/index
# 配置未获授权默认跳转地址
shiro.unauthorizedUrl=/unauthorized
# 是否允许通过 URL 参数实现会话跟踪,默认为 true。如果网站支持 Cookie,可以关闭次选项。
shiro.sessionManager.sessionIdUrlRewritingEnabled=true
# 是否允许通过 Cookie 实现会话跟踪,默认为 true。
shiro.sessionManager.sessionIdCookieEnabled=true
接下来配置Realm,Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。从这个意义上讲,Realm 实质上是一个安全相关的 DAO,它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro。
有两个重要的方法需要介绍:
-
身份验证(getAuthenticationInfo 方法)验证账户和密码,并返回相关信息
-
权限获取(getAuthorizationInfo 方法) 获取指定身份的权限,并返回相关信息
public class MyRealm extends AuthorizingRealm {
/**
* 系统存在的用户列表
*/
private List<String> userList= Stream.of("user1","user2","user3","user3").collect(Collectors.toList());
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
if (!userList.contains(username) || !password.equals("123456")){
throw new UnknownAccountException("用户名或密码错误");
}
return new SimpleAuthenticationInfo(username, password, getName());
}
}
配置Shiro,关于addPathDefinition参数的第二个参数取值如下:
anon: 无需认证即可访问
authc: 需要认证才可访问
user: 点击“记住我”功能可访问
perms: 拥有权限才可以访问
role: 拥有某个角色权限才能访问
也就是我们让doLogin接口可以无需认证即可访问,其余的需要认证才能访问。
@Configuration
public class ShiroConfig {
@Bean
public MyRealm myRealm() {
return new MyRealm();
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm());
return manager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
definition.addPathDefinition("/doLogin", "anon");
definition.addPathDefinition("/**", "authc");
return definition;
}
}
在控制器中通过接收到的用户名和密码构造一个 UsernamePasswordToken,然后获取到Subject对象,调用login执行登录操作。
@RestController
public class LoginController {
/**
* 登录
*
* @param username
* @param password
*/
@PostMapping("/doLogin")
public void doLogin(String username, String password) {
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username, password));
System.out.println("登录成功!");
}
@GetMapping("/index")
public String hello() {
return "hello";
}
@GetMapping("/login")
public String login() {
return "{\"message\":\"请登录\"}";
}
}
全局异常
@RestControllerAdvice
public class ErrorController {
@ExceptionHandler(Exception.class)
public R handlerException(Exception e){
return new R(-1,e.getMessage(),"{}");
}
@ExceptionHandler(UnknownAccountException.class)
public R handlerUnknownAccountException(Exception e){
return new R(-1,e.getMessage(),"{}");
}
}
运行效果
扩展
如果此时某个接口只允许有指定角色的人才能方法的话,可以这样做。
首先使用RequiresRoles注解标明需要具有admin角色的用户才能访问。
@GetMapping("listUser")
@RequiresRoles(value = {"admin"})
public List<String> listUser(){
return Stream.of("1","2","2").collect(Collectors.toList());
}
在doGetAuthorizationInfo中处理逻辑,如果用户是admin1、admin2,则将他们赋予admin角色,也就是只有他两才具有访问listUser的权限。
public class MyRealm extends AuthorizingRealm {
/**
* 系统存在的用户列表
*/
private List<String> userList= Stream.of("user1","user2","user3","user3","admin1","admin2").collect(Collectors.toList());
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String userName = (String) principals.getPrimaryPrincipal();
if ("admin1".equals(userName)||"admin2".equals(userName)){
Set<String> rol =new HashSet<>();
rol.add("admin");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(rol);
return simpleAuthorizationInfo;
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
String password = new String((char[]) token.getCredentials());
if (!userList.contains(username) || !password.equals("123456")){
throw new UnknownAccountException("用户名或密码错误");
}
return new SimpleAuthenticationInfo(username, password, getName());
}
}
首先这是普通用户登录后访问的提示。
在使用admin1用户名进行登录后的截图。
另外RequiresRoles注解中Logical的值默认是Logical.AND。这个值表明用户要具有指定所有角色才允许访问,如果我们想只要满足其中一个角色即可访问的话需要更改为Logical.OR。
@GetMapping("listUser")
@RequiresRoles(value = {"admin","teacher"},logical = Logical.OR)
public List<String> listUser(){
return Stream.of("1","2","2").collect(Collectors.toList());
}
类似还有其他注解
@RequiresAuthentication
要求当前Subject 已经在当前的session 中被验证通过才能被注解的类/实例/方法访问或调用
@RequiresGues
要求当前的Subject 是一个“guest”,也就是他们必须是在之前的session中没有被验证或记住才能被注解的类/实例/方法访问或调用
@RequiresPermissions
要求当前的Subject 被允许一个或多个权限,以便执行注解的方法。
@RequiresUser
需要当前的Subject 是一个应用程序用户才能被注解的类/实例/方法访问或调用。要么是通过验证被确认,或者在之前session 中的’RememberMe’服务被记住