正文
Shiro用starter方式优雅整合到SpringBoot中
网上找到大部分文章都是以前SpringMVC下的整合方式,很多人都不知道shiro提供了官方的starter可以方便地跟SpringBoot整合。本文介绍我的3种整合思路:1.完全使用注解;2.完全使用url配置;3.url配置和注解混用,url配置负责鉴权控制,注解负责权限控制。三种方式各有优劣,需考虑实际应用场景使用。
代码
Talk is cheap, show you my code:
elegant-shiro-boot
这个工程使用gradle构建,有三个子工程:
-
demo1演示只用注解来做鉴权授权
-
demo2演示只用url配置来做鉴权授权
-
demo3演示两种方式结合,url配置负责控制鉴权,注解配置负责控制授权。
如何整合
请看shiro官网关于springboot整合shiro的链接:
Integrating Apache Shiro into Spring-Boot Applications
可笑的是,我自己直接上去官网找,找来找去都找不到这一页的文档,而是通过google找出来的。
这篇文档的介绍也相当简单。我们只需要按照文档说明,引入
shiro-spring-boot-starter
,然后在spring容器中注入一个我们自定义的
Realm
,shiro通过这个realm就可以知道如何获取用户信息来处理
鉴权(Authentication)
,如何获取用户角色、权限信息来处理
授权(Authorization)
。
ps:鉴权可以理解成判断一个用户是否已登录的过程,授权可以理解成判断一个已登录用户是否有访问权限的过程。
整合过程:
1.引入starter,我的是用gradle做项目构建的,maven也是引入对应的依赖即可:
dependencies {
//spring boot的starter
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'org.springframework.boot:spring-boot-starter-aop'
compile 'org.springframework.boot:spring-boot-devtools'
testCompile 'org.springframework.boot:spring-boot-starter-test'
//shiro
compile 'org.apache.shiro:shiro-spring-boot-web-starter:1.4.0'
}
2.编写自定义realm
User.java(其它RBAC模型请看github上的代码com.abc.entity包下的类)
public class User {
private Long uid; // 用户id
private String uname; // 登录名,不可改
private String nick; // 用户昵称,可改
private String pwd; // 已加密的登录密码
private String salt; // 加密盐值
private Date created; // 创建时间
private Date updated; // 修改时间
private Set<String> roles = new HashSet<>(); //用户所有角色值,用于shiro做角色权限的判断
private Set<String> perms = new HashSet<>(); //用户所有权限值,用于shiro做资源权限的判断
//getters and setters...
}
UserService.java
@Service
public class UserService {
/**
* 模拟查询返回用户信息
* @param uname
* @return
*/
public User findUserByName(String uname){
User user = new User();
user.setUname(uname);
user.setNick(uname+"NICK");
user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");//密码明文是123456
user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");//加密密码的盐值
user.setUid(new Random().nextLong());//随机分配一个id
user.setCreated(new Date());
return user;
}
}
RoleService.java
@Service
public class RoleService {
/**
* 模拟根据用户id查询返回用户的所有角色,实际查询语句参考:
* SELECT r.rval FROM role r, user_role ur
* WHERE r.rid = ur.role_id AND ur.user_id = #{userId}
* @param uid
* @return
*/
public Set<String> getRolesByUserId(Long uid){
Set<String> roles = new HashSet<>();
//三种编程语言代表三种角色:js程序员、java程序员、c++程序员
roles.add("js");
roles.add("java");
roles.add("cpp");
return roles;
}
}
PermService.java
@Service
public class PermService {
/**
* 模拟根据用户id查询返回用户的所有权限,实际查询语句参考:
* SELECT p.pval FROM perm p, role_perm rp, user_role ur
* WHERE p.pid = rp.perm_id AND ur.role_id = rp.role_id
* AND ur.user_id = #{userId}
* @param uid
* @return
*/
public Set<String> getPermsByUserId(Long uid){
Set<String> perms = new HashSet<>();
//三种编程语言代表三种角色:js程序员、java程序员、c++程序员
//js程序员的权限
perms.add("html:edit");
//c++程序员的权限
perms.add("hardware:debug");
//java程序员的权限
perms.add("mvn:install");
perms.add("mvn:clean");
perms.add("mvn:test");
return perms;
}
}
CustomRealm.java
/**
* 这个类是参照JDBCRealm写的,主要是自定义了如何查询用户信息,如何查询用户的角色和权限,如何校验密码等逻辑
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermService permService;
//告诉shiro如何根据获取到的用户信息中的密码和盐值来校验密码
{
//设置用于匹配密码的CredentialsMatcher
HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
hashMatcher.setStoredCredentialsHexEncoded(false);
hashMatcher.setHashIterations(1024);
this.setCredentialsMatcher(hashMatcher);
}
//定义如何获取用户的角色和权限的逻辑,给shiro做权限判断
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//null usernames are invalid
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
User user = (User) getAvailablePrincipal(principals);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
System.out.println("获取角色信息:"+user.getRoles());
System.out.println("获取权限信息:"+user.getPerms());
info.setRoles(user.getRoles());
info.setStringPermissions(user.getPerms());
return info;
}
//定义如何获取用户信息的业务逻辑,给shiro做登录
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
// Null username is invalid
if (username == null) {
throw new AccountException("Null usernames are not allowed by this realm.");
}
User userDB = userService.findUserByName(username);
if (userDB == null) {
throw new UnknownAccountException("No account found for admin [" + username + "]");
}
//查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方
//SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息,包括角色和权限
Set<String> roles = roleService.getRolesByUserId(userDB.getUid());
Set<String> perms = permService.getPermsByUserId(userDB.getUid());
userDB.getRoles().addAll(roles);
userDB.getPerms().addAll(perms);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());
if (userDB.getSalt() != null) {
info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt()));
}
return info;
}
}
3.使用注解或url配置,来控制鉴权授权
请参照官网的示例:
//url配置
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
// logged in users with the 'admin' role
chainDefinition.addPathDefinition("/admin/**", "authc, roles[admin]");
// logged in users with the 'document:read' permission
chainDefinition.addPathDefinition("/docs/**", "authc, perms[document:read]");
// all other paths require a logged in user
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
//注解配置
@RequiresPermissions("document:read")
public void readDocument() {
...
}
4.解决spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同时,引入了spring aop的starter,会有一个奇怪的问题,导致shiro注解的请求,不能被映射,需加入以下配置:
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
思路1:只用注解控制鉴权授权
使用注解的优点是控制的粒度细,并且非常适合用来做基于资源的权限控制。
关于基于资源的权限控制,建议看看这篇文章:
The New RBAC: Resource-Based Access Control
只用注解的话非常简单。我们只需要使用url配置配置一下所以请求路径都可以匿名访问:
//在 ShiroConfig.java 中的代码:
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
// 由于demo1展示统一使用注解做访问控制,所以这里配置所有请求路径都可以匿名访问
chain.addPathDefinition("/**", "anon"); // all paths are managed via annotations
// 这另一种配置方式。但是还是用上面那种吧,容易理解一点。
// or allow basic authentication, but NOT require it.
// chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");
return chain;
}
然后在控制器类上使用shiro提供的种注解来做控制:
注解
|
功能
|
@RequiresGuest
|
只有游客可以访问
|
@RequiresAuthentication
|
需要登录才能访问
|
@RequiresUser
|
已登录的用户或“记住我”的用户能访问
|
@RequiresRoles
|
已登录的用户需具有指定的角色才能访问
|
@RequiresPermissions
|
已登录的用户需具有指定的权限才能访问
|
代码示例:(更详细的请参考github代码的demo1)
/**
* created by CaiBaoHong at 2018/4/18 15:51<br>
* 测试shiro提供的注解及功能解释
*/
@RestController
@RequestMapping("/t1")
public class Test1Controller {
// 由于TestController类上没有加@RequiresAuthentication注解,
// 不要求用户登录才能调用接口。所以hello()和a1()接口都是可以匿名访问的
@GetMapping("/hello")
public String hello() {
return "hello spring boot";
}
// 游客可访问,这个有点坑,游客的意思是指:subject.getPrincipal()==null
// 所以用户在未登录时subject.getPrincipal()==null,接口可访问
// 而用户登录后subject.getPrincipal()!=null,接口不可访问
@RequiresGuest
@GetMapping("/guest")
public String guest() {
return "@RequiresGuest";
}
// 已登录用户才能访问,这个注解比@RequiresUser更严格
// 如果用户未登录调用该接口,会抛出UnauthenticatedException
@RequiresAuthentication
@GetMapping("/authn")
public String authn() {
return "@RequiresAuthentication";
}
// 已登录用户或“记住我”的用户可以访问
// 如果用户未登录或不是“记住我”的用户调用该接口,UnauthenticatedException
@RequiresUser
@GetMapping("/user")
public String user() {
return "@RequiresUser";
}
// 要求登录的用户具有mvn:build权限才能访问
// 由于UserService模拟返回的用户信息中有该权限,所以这个接口可以访问
// 如果没有登录,UnauthenticatedException
@RequiresPermissions("mvn:install")
@GetMapping("/mvnInstall")
public String mvnInstall() {
return "mvn:install";
}
// 要求登录的用户具有mvn:build权限才能访问
// 由于UserService模拟返回的用户信息中【没有】该权限,所以这个接口【不可以】访问
// 如果没有登录,UnauthenticatedException
// 如果登录了,但是没有这个权限,会报错UnauthorizedException
@RequiresPermissions("gradleBuild")
@GetMapping("/gradleBuild")
public String gradleBuild() {
return "gradleBuild";
}
// 要求登录的用户具有js角色才能访问
// 由于UserService模拟返回的用户信息中有该角色,所以这个接口可访问
// 如果没有登录,UnauthenticatedException
@RequiresRoles("js")
@GetMapping("/js")
public String js() {
return "js programmer";
}
// 要求登录的用户具有js角色才能访问
// 由于UserService模拟返回的用户信息中有该角色,所以这个接口可访问
// 如果没有登录,UnauthenticatedException
// 如果登录了,但是没有该角色,会抛出UnauthorizedException
@RequiresRoles("python")
@GetMapping("/python")
public String python() {
return "python programmer";
}
}
思路2:只用url配置控制鉴权授权
shiro提供和多个默认的过滤器,我们可以用这些过滤器来配置控制指定url的权限: