本文结合一个简单的权限模块设计来实现Shiro的集成。
新建实体如下:
权限实体Permission:id,code,name,parent_id;
角色实体Role:id,code,name;
用户实体User:id,username,password,role(简化设计,一个用户只能有一个角色,因此User表中设置一个role_id字段关联角色);
Role和Permission的关系通过role_permission关系表维护。
具体见源代码https://github.com/wu-boy/parker.git,parker-shiro-base模块,resources目录下有建表和初始化SQL。
SpringBoot集成Shiro引入shiro-spring-boot-web-starter即可。
首先自定义MyRealm,在这个Realm中做登录认证和用户授权。
package com.wu.parker.shiro.base.shiro;
import com.wu.parker.shiro.base.po.Permission;
import com.wu.parker.shiro.base.po.User;
import com.wu.parker.shiro.base.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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* @author: wusq
* @date: 2018/12/8
*/
public class MyRealm extends AuthorizingRealm {
private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("授权");
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = null;
try {
authorizationInfo = new SimpleAuthorizationInfo();
User user = userService.findByUsername(username);
authorizationInfo.addRole(user.getRole().getCode());
List<Permission> list = user.getRole().getPermissionList();
for(Permission p:list){
authorizationInfo.addStringPermission(p.getCode());
}
} catch (Exception e) {
log.error("授权错误{}", e.getMessage());
e.printStackTrace();
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("登录认证");
String username = (String) token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) {
throw new UnknownAccountException(); // 没找到帐号
}
/*if(Boolean.TRUE.equals(user.getLocked())) {
throw new LockedAccountException(); //帐号锁定
}*/
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以在此判断或自定义实现
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUsername(), //用户名
user.getPassword(), //密码
getName() //realm name
);
return authenticationInfo;
}
}
ShiroConfig配置如下
package com.wu.parker.shiro.base.config;
import com.wu.parker.shiro.base.shiro.MyRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author: wusq
* @date: 2018/12/8
*/
@Configuration
public class ShiroConfig {
/**
* 注入自定义的realm,告诉shiro如何获取用户信息来做登录认证和授权
*/
@Bean
public Realm realm() {
return new MyRealm();
}
/**
* 这里统一做鉴权,即判断哪些请求路径需要用户登录,哪些请求路径不需要用户登录。
* 这里只做鉴权,不做权限控制,因为权限用注解来做。
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
// 设置哪些请求可以匿名访问
chain.addPathDefinition("/login/**", "anon");
// 由于使用Swagger调试,因此设置所有Swagger相关的请求可以匿名访问
chain.addPathDefinition("/swagger-ui.html", "anon");
chain.addPathDefinition("/swagger-resources", "anon");
chain.addPathDefinition("/swagger-resources/configuration/security", "anon");
chain.addPathDefinition("/swagger-resources/configuration/ui", "anon");
chain.addPathDefinition("/v2/api-docs", "anon");
chain.addPathDefinition("/webjars/springfox-swagger-ui/**", "anon");
//除了以上的请求外,其它请求都需要登录
chain.addPathDefinition("/**", "authc");
return chain;
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
creator.setUsePrefix(true);
return creator;
}
}
新建PermissionController用来测试
package com.wu.parker.shiro.base.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: wusq
* @date: 2018/12/8
*/
@Api(description = "资源服务")
@RestController
@RequestMapping("/security/permissions/")
public class PermissionController {
@ApiOperation("查询资源")
@GetMapping()
@RequiresPermissions("permission:retrieve")
public String get(){
return "有permission:retrieve这个权限的用户才能访问,不然访问不了";
}
}
新建LoginController完成登录功能
package com.wu.parker.shiro.base.controller;
import com.wu.parker.shiro.base.po.User;
import com.wu.parker.shiro.base.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: wusq
* @date: 2018/12/8
*/
@Api(description = "登录服务")
@RestController
@RequestMapping("/login/")
public class LoginController {
private static final Logger log = LoggerFactory.getLogger(LoginController.class);
@Autowired
private UserService userService;
@ApiOperation("登录")
@GetMapping("{username}/{password}")
public User login(@PathVariable String username, @PathVariable String password){
User result = null;
Subject subject = SecurityUtils.getSubject();
// 此处的密码应该是按照后台的加密规则加密过的,不应该传输明文密码
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
result = userService.findByUsername(username);
} catch (UnknownAccountException e) {
log.error("用户名或密码错误");
e.printStackTrace();
} catch (IncorrectCredentialsException e) {
log.error("用户名或密码错误");
e.printStackTrace();
} catch (AuthenticationException e) {
//其他错误,比如锁定,如果想单独处理请单独catch处理
log.error("其他错误");
e.printStackTrace();
}
return result;
}
}
启动工程后,可以先访问PermissionController中的路径,会提示404,因为没有登录,被Shiro拦截了。
再测试登录功能,通过正确的用户名和密码登录后,再访问PermissionController会返回正常结果。
相关的注意事项都在代码注释中说明了。
附上加密工具类EncryptUtils,方便对Shiro的加密方式进行理解和测试
package com.wu.parker.common.encrypt;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 加解密工具类
* @author: wusq
* @date: 2018/12/8
*/
public class EncryptUtils {
/**
* 默认加密次数
*/
public static final Integer DEFAULT_ITERATIONS = 1;
/**
* Shiro的MD5加密,加密方式是对字符串salt+password进行加密
* @param salt 盐
* @param password 密码
* @return
*/
public static String shiroMd5(String salt, String password){
String algorithmName = "MD5";
ByteSource byteSalt = ByteSource.Util.bytes(salt);
SimpleHash simpleHash = new SimpleHash(algorithmName, password, byteSalt, DEFAULT_ITERATIONS);
return simpleHash.toHex();
}
/**
* Java的MD5加密,加密方式是对字符串salt+password进行加密
* @param salt 盐
* @param password
* @return
*/
public static String md5(String salt, String password){
String result = null;
byte[] bytes = null;
try {
// 生成一个MD5加密计算摘要
MessageDigest md = MessageDigest.getInstance("MD5");
// 对字符串进行加密
md.update((salt + password).getBytes());
// 获得加密后的数据
bytes = md.digest();
// 将加密后的数据转换为16进制数字
result = new BigInteger(1, bytes).toString(16);// 16进制数字
// 如果生成数字未满32位,需要前面补0
for (int i = 0; i < 32 - result.length(); i++) {
result = "0" + result;
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("没有md5这个算法!");
}
return result;
}
public static void main(String[] args) {
String password1 = shiroMd5("admin", "12345678");
System.out.println(password1);
String password2 = md5("admin", "12345678");
System.out.println(password2);
// 两者加密结果相同
System.out.println(password1.equals(password2));
}
}
源代码
https://github.com/wu-boy/parker.git
parker-shiro-base模块
EncryptUtils位于parker-common模块
参考资料
1、跟我学Shiro
2、Shiro用starter方式优雅整合到SpringBoot中
3、springboot(十四):springboot整合shiro-登录认证和权限管理
4、Shiro登陆异常 did not match the expected credentials. 是为什么