本文結合一個簡單的權限模塊設計來實現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. 是爲什麼