1.配置pom.xml
注意:以下是shiro整合的最小依赖(并不包含orm框架以及其它web应用所依赖的常用框架),本文只讨论shiro的应用。另:本文需要有一定的springboot基础,因为我们会忽略掉涉及到springboot细节,比如启动程序的编写、配置文件等等,只注重于shiro的web集成与使用。
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yyoo.mytest</groupId>
<artifactId>shiro-1</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<!--打包的时候可以不用包进去,别的设施会提供。事实上该依赖理论上可以参与编译,测试,运行等周期。
相当于compile,但是打包阶段做了exclude操作-->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
</dependencies>
</project>
2.自定义用户类MyUser以及MyUserService
MyUser
package com.yyoo.mytest.shiro1.bean;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 自己的user对象,一般和你的数据库设计一致
* 当然你还可以添加部门什么的其他信息
*/
public class MyUser implements Serializable {
private String userName;
private String password;
/**
* 用户角色列表(我们将角色和用户绑定)
*/
private List<String> roles = new ArrayList<String>();
/**
* 用户权限列表
*/
private List<String> permissions = new ArrayList<String>();
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public List<String> getPermissions() {
return permissions;
}
public void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("MyUser{");
sb.append("userName='").append(userName).append('\'');
sb.append(", password='").append(password).append('\'');
sb.append(", roles=").append(roles);
sb.append(", permissions=").append(permissions);
sb.append('}');
return sb.toString();
}
}
MyUserService
package com.yyoo.mytest.shiro1.service;
import com.yyoo.mytest.shiro1.bean.MyUser;
import org.apache.shiro.authc.AuthenticationException;
public interface MyShiroUserService {
/**
* 通过用户名密码获取登录用户
* @param userName 用户名
* @param password 密码(最好是加密的)
* @return 用户对象
* @throws AuthenticationException
* 如果返回null,shiro会抛出UnknownAccountException
* 如果需要抛出其他异常,请自定义异常继承AuthenticationException抛出
* 此写法有点自定义适配器的意味,实际使用中,我们可以实现该接口即可(default写法貌似JDK1.8才支持,如果报错请查看你的JDK版本)
*/
default MyUser getUser(String userName,String password)throws AuthenticationException{
MyUser myUser = new MyUser();
if("anonymous".equals(userName) && "123456".equals(password)) {
myUser.setUserName(userName);
myUser.setPassword(password);
myUser.getRoles().add("anonymous");
myUser.getPermissions().add("p1");
}else {
throw new AuthenticationException("用户名密码错误");
}
return myUser;
}
}
你可以认为此Service是接口和其一个默认实现的合体,其本质就是一个接口。默认实现我们是固定设置了一个用户。实际使用中我们实现该接口即可。该接口用于shiro认证时,通过用户名、密码获取用户信息。
3.自定义Realm
package com.yyoo.mytest.shiro1.realm;
import com.yyoo.mytest.shiro1.bean.MyUser;
import com.yyoo.mytest.shiro1.service.MyShiroUserService;
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;
public class MyRealm1 extends AuthorizingRealm {
private MyShiroUserService myShiroUserService;
public MyRealm1(){
this.myShiroUserService = null;
}
public MyRealm1(MyShiroUserService myShiroUserService) {
this.myShiroUserService = myShiroUserService;
}
/**
* 用户认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 我们login的时候使用的UsernamePasswordToken,所以此处我直接强转了,大家可以根据自己的需要来做
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
// 此处可自定义一个Service来实现数据库的查询以验证用户名、密码
// 当然密码可能需要进行加密比对等,这里我们先写死,一步步来(这里我们再定义一个我们自己的User类MyUser)
MyUser user = myShiroUserService.getUser(token.getUsername(),new String(token.getPassword()));
if(user == null){
// 这里返回后会报出对应异常(用户不存在-用户名密码错误)
return null;
}
// 这里存入user,通过 subject.getPrincipal(); 返回就是对应的MyUser类型
SimpleAuthenticationInfo simpleAuthenticationInfo =
new SimpleAuthenticationInfo(user,
user.getPassword(), getName());
return simpleAuthenticationInfo;
}
/**
* 用户授权(角色权限和对应权限添加到shiro)
* @param principalCollection
* @return
* 注:只有在调用如下几种情况的链接时才会执行该方法
*
* 直接使用checkPermissions或checkoutRoles方法鉴权
* 或者是添加了权限注解的url
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取登录用户Bean(因为下面方法中设置的就是MyUser对象)
// principalCollection就是在上面认证成功后会存在shiro的session的用户信息
// 调用shiro鉴权方法后,用户信息会通过参数传递到此,所以此处直接可以获取当前登录人的信息
MyUser user = (MyUser) principalCollection.getPrimaryPrincipal();
// 这里需要使用获取自定义的用户信息(角色和权限)
// 添加当前用户所拥有的角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 添加角色
simpleAuthorizationInfo.addRoles(user.getRoles());
// 添加权限
simpleAuthorizationInfo.addStringPermissions(user.getPermissions());
return simpleAuthorizationInfo;
}
}
仔细对比你会发现,该实现就是我们前一章的自定义Realm,多定义了一个Service而已
4.shiro相关的配置ShiroConfig
package com.yyoo.mytest.shiro1.config;
import com.yyoo.mytest.shiro1.realm.MyRealm1;
import com.yyoo.mytest.shiro1.service.MyShiroUserService;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
public MyShiroUserService myShiroUserService(){
// -- 默认实现,提供了anonymous/123456一个账号(实际使用中请实现该接口)
MyShiroUserService shiroUserService = new MyShiroUserService() {
};
return shiroUserService;
}
@Bean
public Realm myShiroRealm(MyShiroUserService myShiroUserService) {
Realm myShiroRealm = new MyRealm1(myShiroUserService);
return myShiroRealm;
}
@Bean
public SecurityManager securityManager(Realm realm) {
// 注意:这里的DefaultWebSecurityManager和我们之前的Demo使用的DefaultSecurityManager有区别
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
// 以上是基本配置,但我们在web环境下,还需要一些配置
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> shiroFilterFactoryBeanFilters = shiroFilterFactoryBean.getFilters();
// shiro的authc过滤器(注意这些过滤器是有顺序的,此map类型为LinkedHashMap)
shiroFilterFactoryBeanFilters.put("authc",new FormAuthenticationFilter());
// 此处也应注意加入map的顺序
Map<String, String> filterMap = new LinkedHashMap<String, String>();
// 登出
filterMap.put("/logout", "logout");
// swagger(配置不验证swagger相关页面地址)
filterMap.put("/swagger**/**", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/v2/**", "anon");
filterMap.put("/login/login","anon");// 不认证登录地址
filterMap.put("/static/**","anon");
// 对所有用户授权认证
filterMap.put("/**", "authc");
// authc对应shiroFilterFactoryBeanFilters的key值,logout和anon都是shrio自带的过滤器。
// 用户如果未登录访问需要认证的地址就会跳转到该地址(一般是登录页面,这里我们设置的是本文的地址)
// 可以是这里设置的全路径,也可以说本应用的相对路径
shiroFilterFactoryBean.setLoginUrl("https://blog.csdn.net/forlinkext/article/details/115748941");
// 登录成功后的页面(我们使用的是json方式返回,这里的设置无效)
shiroFilterFactoryBean.setSuccessUrl("/static/success.html");
// 错误页面,认证不通过跳转(我们使用的是json方式返回,这里的设置无效)
shiroFilterFactoryBean.setUnauthorizedUrl("/static/error.html");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 提供权限注解支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
shiro自带的过滤器
| 名称 | 对应类名 | 说明 |
|---|---|---|
| anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 没有参数,表示可以匿名使用 |
| authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 表示需要认证(登录)才能使用,没有参数 |
| authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | 没有参数表示httpBasic认证 |
| authcBearer | org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter | |
| invalidRequest | org.apache.shiro.web.filter.InvalidRequestFilter | |
| logout | org.apache.shiro.web.filter.authc.LogoutFilter | |
| noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | |
| perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 单个时不得加引号,参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms[“user:add:,user:modify:”],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。 |
| port | org.apache.shiro.web.filter.authz.PortFilter | port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。 |
| rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | 根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。 |
| roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles[“admin,guest”],每个参数通过才算通过,相当于hasAllRoles()方法。 |
| ssl | org.apache.shiro.web.filter.authz.SslFilter | 没有参数,表示安全的url请求,协议为https |
| user | org.apache.shiro.web.filter.authc.UserFilter | 没有参数表示必须存在用户,当登入操作时不做检查 |
5.编写登录Controller和鉴权的TestController
统一结果对象MyResponse
package com.yyoo.mytest.shiro1.bean;
/**
*
* 统一结果对象
*
*/
public class MyResponse<T> {
/**
* 应答成功或失败
*/
private boolean success;
/**
* 提示消息
*/
private String msg;
/**
* 业务状态码
*/
private int bizCode;
/**
* 返回结果对象
*/
private T content;
private MyResponse(){}
public static final <T> MyResponse<T> success(){
MyResponse<T> response = new MyResponse<T>();
response.setSuccess(true);
return response;
}
public static final <T> MyResponse<T> success(String msg){
MyResponse<T> response = success();
response.setMsg(msg);
return response;
}
public static final <T> MyResponse<T> success(T content){
MyResponse<T> response = success();
response.setContent(content);
return response;
}
public static final <T> MyResponse<T> success(String msg,T content){
MyResponse<T> response = success();
response.setContent(content);
response.setMsg(msg);
return response;
}
public static final <T> MyResponse<T> error(String msg){
MyResponse<T> response = new MyResponse<T>();
response.setMsg(msg);
response.setSuccess(false);
return response;
}
public static final <T> MyResponse<T> error(String msg,int bizCode){
MyResponse<T> response = error(msg);
response.setBizCode(bizCode);
return response;
}
public static final <T> MyResponse<T> error(String msg,T content){
MyResponse<T> response = error(msg);
response.setContent(content);
return response;
}
public static final <T> MyResponse<T> error(String msg,T content,int bizCode){
MyResponse<T> response = error(msg);
response.setContent(content);
response.setBizCode(bizCode);
return response;
}
//------------------------------------------------------
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getBizCode() {
return bizCode;
}
public void setBizCode(int bizCode) {
this.bizCode = bizCode;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
}
LoginController
package com.yyoo.mytest.shiro1.controller;
import com.yyoo.mytest.shiro1.bean.MyResponse;
import com.yyoo.mytest.shiro1.bean.MyUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("login")
public class LoginController {
@RequestMapping("login")
public MyResponse login(@RequestBody MyUser myUser){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(myUser.getUserName(), myUser.getPassword());
usernamePasswordToken.setRememberMe(true);
try {
subject.login(usernamePasswordToken);
} catch (LockedAccountException e) {
return MyResponse.error("用户被锁定");
} catch (AuthenticationException e){
return MyResponse.error("用户名密码错误");
}
return MyResponse.success("登录成功");
}
}
TestController
package com.yyoo.mytest.shiro1.controller;
import com.yyoo.mytest.shiro1.bean.MyResponse;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping("test1")
@RequiresPermissions(value = {"p1","p2"},logical = Logical.OR)
public MyResponse test1(){
Map<String,Object> map = new HashMap<>();
map.put("name","测试权限1");
return MyResponse.success(map);
}
@RequestMapping("test2")
@RequiresPermissions(value = {"p1","p2"},logical = Logical.AND)
public MyResponse test2(){
Map<String,Object> map = new HashMap<>();
map.put("name","测试权限2");
return MyResponse.success(map);
}
}
6.使用postman进行验证
验证登录
正确的用户名密码

错误的用户名或密码

验证权限
test1验权

我们的anonymous账号拥有p1权限,在test1中Logical.OR表示拥有p1或p2权限即可访问,所以此处正确返回。
test2验权

我们的anonymous账号只拥有p1权限,在test2中Logical.AND表示拥有p1和p2权限即可访问,所以此处没有权限会报错(但是我们看看报错信息,其实不符合我们的返回标准,我们来再加一个全局的异常处理来处理该问题)。
添加一个全局的权限处理器来处理权限验证异常
package com.yyoo.mytest.shiro1.config;
import com.yyoo.mytest.shiro1.bean.MyResponse;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 没有权限的全局异常处理器
*/
@RestControllerAdvice
public class MyUnauthorizedExceptionHandler {
private Logger log = LoggerFactory.getLogger(MyUnauthorizedExceptionHandler.class);
@ExceptionHandler(UnauthorizedException.class)
public MyResponse exceptionHandler(UnauthorizedException e){
if(log.isErrorEnabled()){
log.error("对不起,您无权访问!",e);
}
return MyResponse.error("对不起,您无权访问!");
}
}
重启后,我们再来看看返回值
注意:以上验权测试都是基于已登录的情况,未登录时都会跳转到ShiroConfig中设置的LoginUrl对应的地址(我们示例中配置的是本文的地址,一般情况下是登录页面)
7.登出
登出的设置
在我们的ShiroConfig中有如下设置(我们没有定义任何的登出的Controller)
// 登出
filterMap.put("/logout", "logout");
此设置表示,我们将使用LogoutFilter过滤器来处理登出。地址为/logout
使用postman登出
登出和未登录一样,最后跳转至ShiroConfig中设置的LoginUrl对应的地址(我们示例中配置的是本文的地址,一般情况下是登录页面)。
其实LogoutFilter中有个redirectUrl属性,默认是是"",也就是登出后访问的是"",由于已经退出了,除了配置了anon的地址,其余的本应用的地址都是需要认证的,所以最后跳转到了LoginUrl。如果需要跳转到其他地址,设置redirectUrl属性即可。