1.概述
模仿 Shiro 開發(fā)一個(gè)簡單權(quán)限框架案例。
主要使用了以下幾種設(shè)計(jì)模式:
- 單例模式
- 工廠模式
- 策略模式
- 責(zé)任鏈模式
該權(quán)限框架主要有以下開發(fā)點(diǎn):
- 讀取配置文件
- 密碼加密(策略模式耳璧,工廠模式)
- 身份認(rèn)證(責(zé)任鏈模式嫩舟,單例模式)
- 權(quán)限認(rèn)證
2.各個(gè)模塊
2.1 讀取配置文件
public class Config {
private static Map<String, String> configMap = new HashMap<>();
static {
InputStream in = Config.class.getResourceAsStream("/permission.ini");
DataInputStream dis = new DataInputStream(in);
String str;
try{
while ((str = dis.readLine()) != null){
String[] configs = str.split("=");
if(configs.length == 2){
configMap.put(configs[0].trim(),configs[1].trim());
}
}
dis.close();
}catch (Exception e){
throw new RuntimeException("配置文件不存在");
}
}
public static String get(String name){
return configMap.get(name);
}
public static String get(String name,String defaultValue){
String value = configMap.get(name);
return value == null ? defaultValue : value;
}
}
在上面 Config 類中用到了 IO 流,IO 流本身就是裝飾器模式的一種應(yīng)用。
2.2 密碼加密
密碼加密模式采用 MD5 加密,但是我們要開發(fā)一個(gè)可靈活擴(kuò)展的框架,允許開發(fā)者們自定義加密方式漓雅,并且能夠通過修改配置文件來修改加密方式。這里我們采用了策略模式朽色,其類圖如下:
public interface PasswordEncrypt {
String encrypt(String password);
}
默認(rèn)的 MD5 加密:
public class Md5Encrypt implements PasswordEncrypt {
@Override
public String encrypt(String password) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
md5.update(password.getBytes());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return new BigInteger(1,md5.digest()).toString(16);
}
}
用工廠來創(chuàng)建加密策略類(使用反射機(jī)制動(dòng)態(tài)創(chuàng)建策略類):
public class EncryptFactory {
/**
* md5
* @param clazz 類名
* @return
*/
public static PasswordEncrypt create(String clazz){
try {
Class cls = Class.forName(clazz);
Object obj = cls.newInstance();
if(obj instanceof PasswordEncrypt){
return (PasswordEncrypt)obj;
}else{
throw new RuntimeException("class not found:" + clazz);
}
} catch (Exception e) {
throw new RuntimeException("class not found:" + clazz);
}
}
}
在 EncryptContext 中邻吞,根據(jù)配置文件的配置來動(dòng)態(tài)選擇使用哪種加密策略,默認(rèn) MD5 加密
public class EncryptContext {
private PasswordEncrypt pe;
public EncryptContext() {
String cls = Config.get("encryptType","com.design.pattern.encrypt.Md5Encrypt");
this.pe = EncryptFactory.create(cls);
}
public String encrypt(String password){
return this.pe.encrypt(password);
}
}
測試一下默認(rèn)的 MD5 加密:
String encryptedPwd = (new EncryptContext()).encrypt("123");
System.out.println("加密后:"+encryptedPwd);
2.2.1 自定義加密邏輯
第一葫男,增加加密策略類:
public class MyEncrypt implements PasswordEncrypt {
@Override
public String encrypt(String password) {
return password + " encrypted pwd";
}
}
第二抱冷,把該類配置到配置文件中:
encryptType = test.encrypt.MyEncrypt
2.3 身份認(rèn)證與權(quán)限認(rèn)證
Realm 這個(gè)概念來自于 Shiro 框架,它是用于進(jìn)行身份認(rèn)證和獲取用戶權(quán)限梢褐。在本案例中徘层,Realm 主要有兩個(gè)抽象方法:
- abstract boolean loginAuth(AuthToken token);
- abstract PermissionInfo doGetPermissionInfo(AuthToken token);
第一個(gè)方法用于判斷當(dāng)前用戶是否認(rèn)證成功,在用戶登錄時(shí)將調(diào)用該方法利职。
第二個(gè)方法是獲取當(dāng)前用戶擁有哪些權(quán)限趣效,在判斷用戶是否有某權(quán)限時(shí)調(diào)用該方法。
這兩個(gè)方法都需要開發(fā)者去實(shí)現(xiàn)猪贪。
開發(fā)者可以自定義多個(gè) Realm跷敬,比如 Realm1 驗(yàn)證用戶名密碼,Realm2 用于驗(yàn)證第三方登錄(微信登錄等)热押。在這里我使用了責(zé)任鏈模式西傀,多個(gè) Realm 只要有一個(gè)驗(yàn)證通過,那么該用戶就登錄成功桶癣。
多個(gè)自定義 Realm 將形成一個(gè)責(zé)任鏈拥褂,而形成責(zé)任鏈的步驟將由AuthManager 完成,并且AuthManager 類是一個(gè)單例模式牙寞。
AuthRealm類:
public abstract class AuthRealm {
private AuthRealm successor;
public void setSuccessor(AuthRealm realm){
this.successor = realm;
}
public final boolean auth(AuthToken token){
if(token == null) return false;
//如果驗(yàn)證成功饺鹃,就返回成功
if(this.loginAuth(token)){
return true;
}
//失敗就將請(qǐng)求傳給下一個(gè)責(zé)任處理器
return successor != null && successor.auth(token);
}
/**
* 登錄驗(yàn)證
* @return
*/
protected abstract boolean loginAuth(AuthToken token);
/**
* 權(quán)限驗(yàn)證
* @return
*/
protected abstract PermissionInfo doGetPermissionInfo(AuthToken token);
}
在上面類中,使用到了AuthToken 和 PermissionInfo 间雀,前者是用戶認(rèn)證信息悔详,存放用戶名密碼等,后者是保存權(quán)限信息惹挟,包括“角色”和“權(quán)限”茄螃。
AuthToken類:
public class AuthToken {
private String username;
private String password;
public AuthToken() {
}
public AuthToken(String username, String password) {
this.username = username;
this.password = password;
}
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;
}
}
PermissionInfo 類:
public class PermissionInfo {
private Set<String> permissions;
private Set<String> roles;
public Set<String> getPermissions() {
return permissions;
}
public void setPermissions(Set<String> permissions) {
this.permissions = permissions;
}
public Set<String> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
/**
* 判斷是否有某權(quán)限
* @param permission
* @return
*/
public boolean isPermitted(String permission){
return this.permissions.contains(permission);
}
}
AuthManager類:
public class AuthManager {
private List<AuthRealm> list = new ArrayList<>();
private static AuthManager instance = new AuthManager();
/**
* 私有構(gòu)造方法,讀取配置文件连锯,通過反射機(jī)制生成Realm归苍,并構(gòu)建責(zé)任鏈
*/
private AuthManager() {
String realms = Config.get("realms");
if(realms == null || realms.isEmpty()){
throw new RuntimeException("請(qǐng)定義Realm");
}
String[] clss = realms.split(",");
for (int i = 0;i < clss.length; i++){
try {
Object obj = Class.forName(clss[i]).newInstance();
if(obj instanceof AuthRealm){
this.list.add((AuthRealm)obj);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//形成責(zé)任鏈
for (int i=0;i<list.size()-1;i++){
AuthRealm next = list.get(i+1);
if(next != null){
list.get(i).setSuccessor(next);
}
}
}
/**
* 調(diào)用 Realm 中的 DoGetPermissionInfo 方法用狱,如果有多個(gè) Realm,只調(diào)用第一個(gè)
* @param token
* @return
*/
public PermissionInfo getPermissionInfo(AuthToken token){
if(token == null){
return null;
}
if(list.size() > 0){
return this.list.get(0).doGetPermissionInfo(token);
}
return null;
}
/**
* 登錄認(rèn)證拼弃,調(diào)用 Realm 責(zé)任鏈
* @return
*/
public boolean auth(AuthToken token){
if(list.size() == 0){
return false;
}
return list.get(0).auth(token);
}
/**
* 單例
* @return
*/
public static AuthManager getInstance(){
return instance;
}
}
AuthManager 主要有以下職責(zé):
- 生成 Realm 責(zé)任鏈
- 調(diào)用身份認(rèn)證和權(quán)限認(rèn)證方法
2.4 主體類
還缺一個(gè)用戶主體類Subject夏伊。所謂的用戶主體,有點(diǎn)類似 Web 開發(fā)中的 Session肴敛,一個(gè)用戶請(qǐng)求對(duì)應(yīng)一個(gè)Session,而在權(quán)限框架中吗购,用戶主體類 Subject 就代表了當(dāng)前用戶医男。
Auth 接口,定義了登錄捻勉,權(quán)限等方法:
public interface Auth {
/**
* 登錄操作
* @param token
* @return
*/
boolean login(AuthToken token);
/**
* 是否已登錄
* @return
*/
boolean isLogin();
/**
* 是否有權(quán)限
* @param permission
* @return
*/
boolean isPermitted(String permission);
}
用戶主體類 Subject :
/**
* 登錄用戶主體
*/
public class Subject implements Auth{
private AuthToken token;
@Override
public boolean login(AuthToken token) {
//調(diào)用密碼加密策略
String password = (new EncryptContext()).encrypt(token.getPassword());
token.setPassword(password);
//調(diào)用auth方法镀梭,即觸發(fā)責(zé)任鏈
if(AuthManager.getInstance().auth(token)){
System.out.println("登錄成功");
this.token = token;
return true;
}
return false;
}
@Override
public boolean isLogin() {
return token != null;
}
@Override
public boolean isPermitted(String permission) {
PermissionInfo info = AuthManager.getInstance().getPermissionInfo(this.token);
return info != null && info.isPermitted(permission);
}
public String getUsername(){
return token.getUsername();
}
}
工具類 SecurityUtils,提供了全局獲取用戶主體類 Subject 的方法:
public class SecurityUtils {
private static Map<String, Subject> subjectList = new HashMap<>();
/**
* 獲取當(dāng)前請(qǐng)求的用戶
* @return
*/
public static Subject getSubject(){
//此處應(yīng)借用 Session 等方式獲取當(dāng)前請(qǐng)求用戶
String name = "123";
Subject subject = subjectList.get(name);
return subject == null ? new Subject() : subject;
}
public static void addSubject(Subject subject){
subjectList.put("123",subject);
}
}
實(shí)際上上面SecurityUtils 中的 getSubject() 的實(shí)現(xiàn)機(jī)制也應(yīng)該是一個(gè)類似 Session 的機(jī)制踱启,就像我們?cè)?Web 請(qǐng)求中獲取當(dāng)前Session报账,Session 和當(dāng)前用戶對(duì)應(yīng)。但本次案例主要是介紹設(shè)計(jì)模式埠偿,就不去實(shí)現(xiàn)那么復(fù)雜的功能了透罢,因此這里就簡單地直接給出 Subject 了。
2.5 整體結(jié)構(gòu)圖
2.6 測試
創(chuàng)建 Realm1:
public class Realm1 extends AuthRealm {
@Override
protected boolean loginAuth(AuthToken token) {
System.out.println("===Realm1 loginAuth===");
String username = token.getUsername();
String pwd = token.getPassword();
//傳進(jìn)來的密碼是加密過的密碼冠蒋,直接和數(shù)據(jù)庫中的密碼比對(duì)
System.out.println("pwd:"+pwd);
//查詢數(shù)據(jù)庫操作略過
return false;
}
@Override
protected PermissionInfo doGetPermissionInfo(AuthToken token) {
String username = token.getUsername();
System.out.println("doGetPermissionInfo1");
//從數(shù)據(jù)庫讀取該用戶的權(quán)限信息
PermissionInfo info = new PermissionInfo();
Set<String> s = new HashSet<String>();
s.add("permission1");
s.add("permission2");
info.setPermissions(s);
//角色
Set<String> r = new HashSet<String>();
r.add("role1");
info.setRoles(r);
return info;
}
}
再創(chuàng)建一個(gè) Realm2羽圃,Realm2 的和1結(jié)構(gòu)是一樣的,具體的業(yè)務(wù)邏輯要根據(jù)你項(xiàng)目實(shí)際情況去修改抖剿,這里只是測試朽寞,就直接給出一模一樣的代碼:
public class Realm2 extends AuthRealm {
@Override
protected boolean loginAuth(AuthToken token) {
System.out.println("===Realm2 loginAuth===");
String username = token.getUsername();
String pwd = token.getPassword();
//傳進(jìn)來的密碼是加密過的密碼,直接和數(shù)據(jù)庫中的密碼比對(duì)
System.out.println("pwd:"+pwd);
//查詢數(shù)據(jù)庫操作略過
return true;
}
@Override
protected PermissionInfo doGetPermissionInfo(AuthToken token) {
String username = token.getUsername();
System.out.println("doGetPermissionInfo2");
//從數(shù)據(jù)庫讀取該用戶的權(quán)限信息
PermissionInfo info = new PermissionInfo();
Set<String> s = new HashSet<String>();
s.add("printer:print");
s.add("printer:query");
info.setPermissions(s);
//角色
Set<String> r = new HashSet<String>();
r.add("role1");
info.setRoles(r);
return info;
}
}
然后將兩個(gè) Realm 配置到配置文件中斩郎,多個(gè) Realm 用逗號(hào)隔開:
encryptType = test.encrypt.MyEncrypt
realms=test.realm.Realm1,test.realm.Realm2
測試代碼:
public class TestDemo {
public static void main(String[] args) throws IOException{
//測試密碼加密
String encryptedPwd = (new EncryptContext()).encrypt("123");
System.out.println("加密后:"+encryptedPwd);
//獲取當(dāng)前用戶
Subject currentUser = SecurityUtils.getSubject();
//是否登錄
System.out.println("是否已登錄:"+currentUser.isLogin());
//執(zhí)行登錄操作
currentUser.login(new AuthToken("admin","123"));
//是否登錄
System.out.println("是否已登錄:"+currentUser.isLogin());
//是否有權(quán)限脑融,權(quán)限用字符串表示
System.out.println("是否有權(quán)限:"+currentUser.isPermitted("permission1"));
}
}
測試結(jié)果:
3.總結(jié)
僅僅看是沒用的,看10遍真的不如自己寫一遍缩宜。找一個(gè)知名的開源框架肘迎,了解其工作流程后,嘗試著用設(shè)計(jì)模式自己去寫一個(gè)簡單的例子锻煌,例如寫個(gè) SpringMVC膜宋。就像本節(jié)課的權(quán)限框架的例子,就是仿照 Shiro 框架寫的炼幔。