1、功能实现
1.开启注解功能,使用并比较注解
2.解决注解式,登录页和无权访问页面不跳转问题
3.拦截器跟注解同时使用的执行顺序
2、shiro03子工程
本篇以 记住我篇 为基础
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yzm</groupId>
<artifactId>shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>shiro03</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>shiro03</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>com.yzm</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.192.128:3306/testdb2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
password: 1234
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.yzm.shiro03.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3、认证和授权
package com.yzm.shiro02.config;
import com.yzm.shiro02.entity.Permissions;
import com.yzm.shiro02.entity.Role;
import com.yzm.shiro02.entity.User;
import com.yzm.shiro02.service.PermissionsService;
import com.yzm.shiro02.service.RoleService;
import com.yzm.shiro02.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 自定义Realm,实现认证和授权
* AuthorizingRealm 继承 AuthorizingRealm
* AuthorizingRealm 提供 授权方法 doGetAuthorizationInfo
* AuthorizingRealm 提供 认证方法 doGetAuthenticationInfo
*/
public class MyShiroRealm extends AuthorizingRealm {
private final UserService userService;
private final RoleService roleService;
private final PermissionsService permissionsService;
public MyShiroRealm(UserService userService, RoleService roleService, PermissionsService permissionsService) {
this.userService = userService;
this.roleService = roleService;
this.permissionsService = permissionsService;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) principalCollection.getPrimaryPrincipal();
// 查询用户,获取角色ids
User user = userService.lambdaQuery().eq(User::getUsername, username).one();
List<Integer> roleIds = Arrays.stream(user.getRIds().split(","))
.map(Integer::parseInt)
.collect(Collectors.toList());
// 查询角色,获取角色名、权限ids
List<Role> roles = roleService.listByIds(roleIds);
Set<String> roleNames = new HashSet<>(roles.size());
Set<Integer> permIds = new HashSet<>();
roles.forEach(role -> {
roleNames.add(role.getRName());
Set<Integer> collect = Arrays.stream(
role.getPIds().split(",")).map(Integer::parseInt).collect(Collectors.toSet());
permIds.addAll(collect);
});
// 获取权限名称
List<Permissions> permissions = permissionsService.listByIds(permIds);
List<String> permNames = permissions.stream().map(Permissions::getPName).collect(Collectors.toList());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roleNames);
authorizationInfo.addStringPermissions(permNames);
return authorizationInfo;
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户名跟密码
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
// 查询用户是否存在
User user = userService.lambdaQuery().eq(User::getUsername, username).one();
if (user == null) {
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
// 用户名 + 盐
ByteSource.Util.bytes(user.getUsername() + user.getSalt()),
getName()
);
}
}
4、ShiroConfig 配置类
package com.yzm.shiro03.config;
import com.yzm.shiro03.service.PermissionsService;
import com.yzm.shiro03.service.RoleService;
import com.yzm.shiro03.service.UserService;
import com.yzm.shiro03.utils.EncryptUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
@Configuration
public class ShiroConfig {
private final UserService userService;
private final RoleService roleService;
private final PermissionsService permissionsService;
public ShiroConfig(UserService userService, RoleService roleService, PermissionsService permissionsService) {
this.userService = userService;
this.roleService = roleService;
this.permissionsService = permissionsService;
}
/**
* 凭证匹配器
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName(EncryptUtils.ALGORITHM_NAME);
hashedCredentialsMatcher.setHashIterations(EncryptUtils.HASH_ITERATIONS);
return hashedCredentialsMatcher;
}
/**
* 自定义Realm
*/
@Bean
public MyShiroRealm shiroRealm() {
MyShiroRealm shiroRealm = new MyShiroRealm(userService, roleService, permissionsService);
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* 记住我功能
*/
@Bean
public Cookie simpleCookie() {
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设为true后,只能通过http访问,javascript无法访问
//防止xss读取cookie
cookie.setHttpOnly(true);
cookie.setPath("/");
//存活时间,单位秒;-1表示关闭浏览器该cookie失效
cookie.setMaxAge(120);
return cookie;
}
@Bean
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
rememberMeManager.setCookie(simpleCookie());
//cookie加密的密钥
rememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return rememberMeManager;
}
/**
* 安全管理SecurityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置realm
securityManager.setRealm(shiroRealm());
// 记住我功能
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
shiroFilterFactoryBean.setLoginUrl("/login"); // 登录页url,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setUnauthorizedUrl("/401"); // 访问无权限跳转url
return shiroFilterFactoryBean;
}
}
5、注解使用
在ShiroConfig中添加如下内容
/**
* 开启注解方式控制访问url
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
@RequiresRoles 等同于 roles
@RequiresPermissions 等同于 perms
@RequiresAuthentication 等同于 authc
@RequiresUser 等同于 user
@RequiresGuest 跟 @RequiresUser 完全相反,即未登录也不能使用记住我功能,只能是游客
HomeController
修改登录接口,进行登录成功或失败重定向
自定义退出接口,不使用Shiro默认的
新增3个接口
package com.yzm.shiro03.controller;
import com.yzm.shiro03.entity.User;
import com.yzm.shiro03.service.UserService;
import com.yzm.shiro03.utils.EncryptUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresGuest;
import org.apache.shiro.authz.annotation.RequiresUser;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HomeController {
private final UserService userService;
public HomeController(UserService userService) {
this.userService = userService;
}
@GetMapping(value = {"/", "/home"})
public String home(ModelMap map) {
Subject subject = SecurityUtils.getSubject();
map.addAttribute("subject", subject.getPrincipals());
return "home";
}
@GetMapping("login")
public String login() {
return "login";
}
@GetMapping("401")
public Object notRole() {
return "401";
}
@PostMapping("register")
public Object register(ModelMap map, @RequestParam String username, @RequestParam String password) {
User user = new User();
user.setUsername(username);
user.setPassword(password);
// 密码加密
EncryptUtils.encryptPassword(user);
userService.save(user);
map.addAttribute("user", user);
return "home";
}
@PostMapping("login")
public Object login(@RequestParam String username, @RequestParam String password, boolean rememberMe) {
// 1.创建UsernamePasswordToken
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
// 使用记住我功能
usernamePasswordToken.setRememberMe(rememberMe);
// 2.创建Subject 用户主体
Subject subject = SecurityUtils.getSubject();
String url = "/home";
try {
// 3.前期准备后,开始登录
subject.login(usernamePasswordToken);
} catch (IncorrectCredentialsException e) {
url = "/login?failure";
} catch (AuthenticationException e) {
url = "/login";
}
return "redirect:" + url;
}
@PostMapping("/logout")
public Object logout() {
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated() || subject.isRemembered()) {
subject.logout();
}
return "redirect:/login";
}
@GetMapping("requiresGuest")
@RequiresGuest
@ResponseBody
public Object requiresGuest() {
return "requiresGuest";
}
@GetMapping("requiresAuthentication")
@RequiresAuthentication
@ResponseBody
public Object requiresAuthentication() {
return "requiresAuthentication";
}
@GetMapping("requiresUser")
@RequiresUser
@ResponseBody
public Object requiresUser() {
return "requiresUser";
}
}
UserController、AdminController 使用注解控制权限访问
package com.yzm.shiro03.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
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("/admin")
@RequiresRoles("ADMIN")
public class AdminController {
@GetMapping
public Object admin() {
return SecurityUtils.getSubject().getPrincipal();
}
@GetMapping("select")
@RequiresPermissions("admin:select")
public Object select() {
return "Select";
}
@GetMapping("create")
@RequiresPermissions("admin:create")
public Object create() {
return "Create";
}
@GetMapping("update")
@RequiresPermissions("admin:update")
public Object update() {
return "Update";
}
@GetMapping("delete")
@RequiresPermissions("admin:delete")
public Object delete() {
return "Delete";
}
}
package com.yzm.shiro03.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
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("//user")
@RequiresRoles(value = {"USER", "ADMIN"}, logical = Logical.OR)
public class UserController {
@GetMapping
public Object user() {
return SecurityUtils.getSubject().getPrincipal();
}
@GetMapping("select")
@RequiresPermissions("user:select")
public Object select() {
return "Select";
}
@GetMapping("create")
@RequiresPermissions("user:create")
public Object create() {
return "Create";
}
@GetMapping("update")
@RequiresPermissions("user:update")
public Object update() {
return "Update";
}
@GetMapping("delete")
@RequiresPermissions("user:delete")
public Object delete() {
return "Delete";
}
@GetMapping("createAndUpdate")
//需要同时拥有
@RequiresPermissions(value = {"user:create", "user:update"}, logical = Logical.AND)
public Object createAndUpdate() {
return "Create And Update";
}
@GetMapping("createOrUpdate")
//拥有其中任意一个即可
@RequiresPermissions(value = {"user:create", "user:update"}, logical = Logical.OR)
public Object createOrUpdate() {
return "Create Or Update";
}
}
6、html页面
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>当前登录状态:<span th:text="${subject == null ? '未登录 ' : '已登录'}"></span></h1>
<h2>1.注册 <span th:if="${user != null}"> [[${user.username}]] 成功</span></h2>
<form action="/register" method="post">
<p>
<label for="username">Username</label>
<input type="text" id="username" name="username" placeholder="Username">
</p>
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password" placeholder="Password">
</p>
<button type="submit">Register</button>
</form>
<h2>2.注销</h2>
<form action="/logout" method="post">
<button type="submit">Sign out</button>
</form>
<h3>3.User 接口列表</h3>
<h4>
<a href="/user">User角色</a>
</h4>
<p><a href="/user/select">User角色,拥有 select 权限</a></p>
<p><a href="/user/create">User角色,拥有 create 权限</a></p>
<p><a href="/user/update">User角色,拥有 update 权限</a></p>
<p><a href="/user/delete">User角色,拥有 delete 权限</a></p>
<p><a href="/user/createAndUpdate">User角色,拥有 create and update 权限</a></p>
<h3>4.Admin 接口列表</h3>
<h4>
<a href="/admin">Admin角色</a>
</h4>
<p><a href="/admin/select">Admin角色,拥有 select 权限</a></p>
<p><a href="/admin/create">Admin角色,拥有 create 权限</a></p>
<p><a href="/admin/update">Admin角色,拥有 update 权限</a></p>
<p><a href="/admin/delete">Admin角色,拥有 delete 权限</a></p>
<h3>注解比较</h3>
<p><a href="/requiresGuest">@RequiresGuest 注解</a></p>
<p><a href="/requiresAuthentication">@RequiresAuthentication 注解</a></p>
<p><a href="/requiresUser">@RequiresUser 注解</a></p>
</body>
</html>
login.html、401.html 参考上一篇
7、测试页面跳转
启动项目,访问/home,未登录,点击user
页面报错,并且控制台打印错误信息
shiroFilterFactoryBean.setLoginUrl("/login");
这行代码不起作用了,先不理它,一会解决
访问/login 登录yzm,跳转到主页,点击Admin角色
跟上面一样页面报错,控制台打印错误信息
shiroFilterFactoryBean.setUnauthorizedUrl("/401"); 一样无效
这是因为注解模式下,登录失败与没有权限都是通过抛出异常,并且默认没有去处理或者捕获这些异常。
有两种解决方案
第一种:设置全局捕获
package com.yzm.shiro03.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Shiro注解模式下,登录失败与没有权限都是通过抛出异常,并且默认并没有去处理或者捕获这些异常。
* 解决方式,通过自定义全局异常捕获处理
*/
@Slf4j
@RestControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler
public void ErrorHandler(AuthorizationException e, HttpServletRequest request, HttpServletResponse response) throws IOException {
log.error("注解方式,自定义异常捕获");
if (e instanceof UnauthenticatedException) {
// 重定向
WebUtils.issueRedirect(request, response, "/login");
} else if (e instanceof UnauthorizedException) {
WebUtils.issueRedirect(request, response, "/401");
}
}
}
重启项目,未登录访问 User角色 跳转到登录页
登录yzm,访问 Admin 角色,跳转到无权页面
第二种:通过在SpringMVC下配置捕获相应异常
在ShiroConfig中,添加
/**
* 问题:未登录不会自动跳转到登录页、无权访问页面不跳转
* 原因:Shiro注解模式下,登录失败与没有权限都是通过抛出异常,并且默认并没有去处理或者捕获这些异常。
* 解决:通过在SpringMVC下配置捕获相应异常来通知用户信息
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
// 未登录访问接口跳转到/login、登录后没有权限跳转到/401
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "redirect:/login");
properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "redirect:/401");
simpleMappingExceptionResolver.setExceptionMappings(properties);
return simpleMappingExceptionResolver;
}
注意:
1.使用的是重定向:“redirect:/login”;而不是转发:"/login",使用转发的话,能跳转到登录页,但表单提交会报错
改回properties.setProperty(“org.apache.shiro.authz.UnauthenticatedException”, “redirect:/login”);
2.注释掉全局捕获 以免影响第二种方法的测试
重启项目,测试结果跟第一种一样
8、注解比较
已登录,未记住我,重开浏览器之后,就成了未登录
@RequiresGuest:未登录可以访问;认证过或使用记住我功能拒绝访问
@RequiresAuthentication: 认证过可以访问,其他时候拒绝访问
@RequiresUser: 认证过或使用记住我功能可以访问
@RequiresPermissions(value = {“user:create”, “user:update”}, logical = Logical.AND)
同时具备2个权限才能访问
@RequiresPermissions(value = {“user:create”, “user:update”}, logical = Logical.OR)
拥有其中任意一个权限就可以访问
@RequiresRoles 跟 @RequiresPermissions 使用差不多的
9、自定义错误页面
在ShiroConfig中,添加
/**
* 自定义错误页面
*/
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
return factory -> {
// 这种可以进行转发,但在处理跳转到登录页之后,进行提交效果不好,这里知道就行
//ErrorPage errorLoginPage = new ErrorPage(UnauthenticatedException.class, "/login");
//ErrorPage error401Page = new ErrorPage(UnauthorizedException.class, "/401");
ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/401");
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/404");
ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500");
factory.addErrorPages(error401Page, error404Page, error500Page);
};
}
在HomeController 新增接口
@GetMapping("/fail")
@ResponseBody
public Object fail() {
int i = 1/0;
return "出错了";
}
@GetMapping("404")
public Object notFound() {
return "404";
}
@GetMapping("500")
public Object error() {
return "500";
}
新增404、500页面,内容跟401差不多
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>500</title>
</head>
<body>
<h1>服务器崩溃了</h1>
</body>
</html>
重启,访问/fail 跳转到500页面,访问不存在的url跳转到404页面
10、注解跟拦截器同时使用,拦截器优先
例如:在ShiroConfig#shiroFilter中,对/user/** 进行roles[ADMIN]拦截
@Bean
public ShiroFilterFactoryBean shiroFilter() {
...
Map<String, String> definitionMap = new LinkedHashMap<>();
definitionMap.put("/user/**", "roles[ADMIN]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);
return shiroFilterFactoryBean;
}
debug role注解处理器和role拦截器
重启,登录yzm,访问/user,先进的是拦截器,但拦截器配置的需要admin角色,yzm只有user角色
所以直接跳转到401页面,不再继续注解处理器
如果拦截器通过,就还会继续注解处理器,只有都通过才能访问