参考地址:http://www.iocoder.cn/Spring-Boot/Shiro/
《认证 (authentication) 和授权 (authorization) 的区别》
以打飞机举例子:
- 【认证】你要登机,你需要出示你的 passport 和 ticket,passport 是为了证明你张三确实是你张三,这就是 authentication。
- 【授权】而机票是为了证明你张三确实买了票可以上飞机,这就是 authorization。
以论坛举例子:
- 【认证】你要登录论坛,输入用户名张三,密码 1234,密码正确,证明你张三确实是张三,这就是 authentication。
- 【授权】再一 check 用户张三是个版主,所以有权限加精删别人帖,这就是 authorization 。
简单来说:认证解决“你是谁”的问题,授权解决“你能做什么”的问题
pom
<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>
配置shiro
package cn.iocoder.springboot.lab01.shirodemo.config;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* SimpleAccountRealm 是使用内存作为数据源,我们可以手动往里面添加用户、角色、权限等数据。
* @return
*/
@Bean
public Realm realm() {//作用:身份验证和授权
// 创建 SimpleAccountRealm 对象
SimpleAccountRealm realm = new SimpleAccountRealm();
// 添加两个用户。参数分别是 username、password、roles 。
realm.addAccount("admin", "admin", "ADMIN");
realm.addAccount("normal", "normal", "NORMAL");
return realm;
}
@Bean
public DefaultWebSecurityManager securityManager() {
// 创建 DefaultWebSecurityManager 对象
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置其使用的 Realm
securityManager.setRealm(this.realm());
return securityManager;
}
/**
* #setLoginUrl(String loginUrl) 方法,设置登录 URL 。在 Shiro 中,约定 GET loginUrl 为登录页面,POST loginUrl 为登录请求。
* #setSuccessUrl(String successUrl) 方法,设置登录成功 URL 。在登录成功时,会重定向到该 URL 上。
* #etUnauthorizedUrl(String unauthorizedUrl) 方法,设置无权限的 URL 。在请求校验权限不通过时,会重定向到该 URL 上
* 上述的 URL 对应的接口,都需要我们自己来实现
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
// 创建 ShiroFilterFactoryBean 对象,用于创建 ShiroFilter 过滤器
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// 设置 SecurityManager
filterFactoryBean.setSecurityManager(this.securityManager());
// 设置 URL 们
filterFactoryBean.setLoginUrl("/login"); // 登陆 URL
filterFactoryBean.setSuccessUrl("/login_success"); // 登陆成功 URL
filterFactoryBean.setUnauthorizedUrl("/unauthorized"); // 无权限 URL
// 设置 URL 的权限配置
filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());
return filterFactoryBean;
}
/**
* 比较常用的过来器有:
*
* anon :AnonymousFilter :允许匿名访问,即无需登录。
* authc :FormAuthenticationFilter :需要经过认证的用户,才可以访问。如果是匿名用户,则根据 URL 不同,会有不同的处理:
* 如果拦截的 URL 是 GET loginUrl 登录页面,则进行该请求,跳转到登录页面。
* 如果拦截的 URL 是 POST loginUrl 登录请求,则基于请求表单的 username、password 进行认证。认证通过后,默认重定向到 GET loginSuccessUrl 地址。
* 如果拦截的 URL 是其它 URL 时,则记录该 URL 到 Session 中。在用户登录成功后,重定向到该 URL 上。
* logout :LogoutFilter :拦截的 URL ,执行退出操作。退出完成后,重定向到 GET loginUrl 登录页面。
* roles :RolesAuthorizationFilter :拥有指定角色的用户可访问。
* perms :PermissionsAuthorizationFilter :拥有指定权限的用户可以访问
* @return
*/
private Map<String, String> filterChainDefinitionMap() {
Map<String, String> filterMap = new LinkedHashMap<>(); // 注意要使用有序的 LinkedHashMap ,顺序匹配
filterMap.put("/test/demo", "anon"); // 允许匿名访问
filterMap.put("/test/admin", "roles[ADMIN]"); // 需要 ADMIN 角色
filterMap.put("/test/normal", "roles[NORMAL]"); // 需要 NORMAL 角色
filterMap.put("/logout", "logout"); // 退出
filterMap.put("/**", "authc"); // 默认剩余的 URL ,需要经过认证
return filterMap;
}
}
controller
package cn.iocoder.springboot.lab01.shirodemo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequestMapping("/")
public class SecurityController {
private Logger logger = LoggerFactory.getLogger(getClass());
@GetMapping("/login")
public String loginPage() {
return "login.html";
}
/**
* 对于登录请求,会被我们配置的 Shiro FormAuthenticationFilter 过滤器进行拦截,进行用户的身份认证。整个过程如下:
*
* FormAuthenticationFilter 解析请求的 username、password 参数,创建 UsernamePasswordToken 对象。
* 然后,调用 SecurityManager 的 #login(Subject subject, AuthenticationToken authenticationToken) 方法,执行登录操作,进行“身份验证”(认证)。
* 在这内部中,调用 Realm 的 #getAuthenticationInfo(AuthenticationToken token) 方法,进行认证。此时,根据认证的是否成功,会有不同的处理:
* 如果认证通过,则 FormAuthenticationFilter 会将请求重定向到 GET loginSuccess 地址上。
* 【重要】如果认证失败,则会将认证失败的原因设置到请求的 attributes 中,后续该请求会继续请求到 POST login 地址上。这样,在 POST loginUrl 地址上,我们可以从 attributes 中获取到失败的原因,提示给用户。
*
* 所以,POST loginUrl 的目的,实际是为了处理认真失败的情况
*/
@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 "你没有权限";
}
}
测试controller
package cn.iocoder.springboot.lab01.shirodemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 权限测试
*/
@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("/normal")
public String normal() {
return "我是普通用户";
}
}
启动类
package cn.iocoder.springboot.lab01.shirodemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
使用注解方式
package cn.iocoder.springboot.lab01.shirodemo.controller;
import org.apache.shiro.authz.annotation.RequiresGuest;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/demo")
public class DemoController {
/**
* 验证不起作用,暂时无法解决
* @return
*/
@RequiresGuest
@GetMapping("/echo")
public String demo() {
return "示例返回";
}
@RequiresGuest
@GetMapping("/aaa")
public String aaa() {
return "aaa示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@RequiresRoles("ADMIN")
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@RequiresRoles("NORMAL")
@GetMapping("/normal")
public String normal() {
return "我是普通用户";
}
}
3.1 @RequiresGuest
@RequiresGuest
注解,和 anon
等价。
3.2 @RequiresAuthentication
@RequiresAuthentication
注解,和 authc
等价。
3.3 @RequiresUser
@RequiresUser
注解,和 user
等价,要求必须登录。
3.4 @RequiresRoles
@RequiresRoles
注解,和 roles
等价。
// 属于 NORMAL 角色
@RequiresRoles("NORMAL")
// 要同时拥有 ADMIN 和 NORMAL 角色
@RequiresRoles({"ADMIN", "NORMAL"})
// 拥有 ADMIN 或 NORMAL 任一角色即可
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)
3.5 @RequiresPermissions
@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)