天天看點

認證授權示例——JWT及Shiro

1、常見的認證機制

1.1 HTTP Basic Auth

HTTP Basic Auth簡單點說明就是每次請求API時都提供使用者的usernamepassword,簡言之,Basic Auth是配合RESTful API 使用的最簡單的認證方式,隻需提供使用者名密碼即可,但由于有把使用者名密碼暴露給第三方用戶端的風險,在生産環境下被使用的越來越少。是以,在開發對外開放的RESTful API時,盡量避免采用HTTP Basic Auth

1.2 Cookie Auth

Cookie認證機制就是為一次請求認證在服務端建立一個Session對象,同時在用戶端的浏覽器端建立了一個Cookie對象;通過用戶端帶上來Cookie對象來與伺服器端session對象比對來實作狀态管理的。預設的,當我們關閉浏覽器的時候,cookie會被删除但可以通過修改cookie 的expire time使cookie在一定時間内有效。

1.3 OAuth

OAuth(開放授權)是一個開放的授權标準,允許使用者讓第三方應用通路該使用者在某一web服務上存儲的私密的資源(如照片,視訊,聯系人清單),而無需将使用者名和密碼提供給第三方應用。 OAuth允許使用者提供一個令牌,而不是使用者名和密碼來通路他們存放在特定服務提供者的資料。每一個令牌授權一個特定的第三方系統(例如,視訊編輯網站)在特定的時段(例如,接下來的2小時内)内通路特定的資源(例如僅僅是某一相冊中的視訊)。這樣,OAuth讓使用者可以授權第三方網站通路他們存儲在另外服務提供者的某些特定資訊,而非所有内容。這種基于OAuth的認證機制适用于個人消費者類的網際網路産品,如社交類APP等應用,但是不太适合擁有自有認證權限管理的企業應用。

認證授權示例——JWT及Shiro

1.4 Token Auth

使用基于 Token 的身份驗證方法,在服務端不需要存儲使用者的登入記錄。大概的流程是這樣的:

  1. 用戶端使用使用者名跟密碼請求登入
  2. 服務端收到請求,去驗證使用者名與密碼
  3. 驗證成功後,服務端會簽發一個 Token,再把這個 Token 發送給用戶端
  4. 用戶端收到 Token 以後可以把它存儲起來,比如放在 Cookie 裡
  5. 用戶端每次向服務端請求資源的時候需要帶着服務端簽發的 Token
  6. 服務端收到請求,然後去驗證用戶端請求裡面帶着的 Token,如果驗證成功,就向用戶端傳回請求的資料
    認證授權示例——JWT及Shiro

    Token Auth的優點:

    支援跨域通路: Cookie是不允許垮域通路的,這一點對Token機制是不存在的,前提是傳輸的使用者認證資訊通過HTTP頭傳輸;

    無狀态(也稱:服務端可擴充行):Token機制在服務端不需要存儲session資訊,因為Token 自身包含了所有登入使用者的資訊,隻需要在用戶端的cookie或本地媒體存儲狀态資訊;

    更适用CDN: 可以通過内容分發網絡請求你服務端的所有資料(如:javascript,HTML,圖檔等),而你的服務端隻要提供API即可;

    去耦: 不需要綁定到一個特定的身份驗證方案。Token可以在任何地方生成,隻要在你的API被調用的時候,你可以進行Token生成調用即可;

    更适用于移動應用: 當你的用戶端是一個原生平台(iOS, Android,Windows 8等)時,Cookie是不被支援的(你需要通過Cookie容器進行處理),這時采用Token認證機制就會簡單得多;

    CSRF:因為不再依賴于Cookie,是以你就不需要考慮對CSRF(跨站請求僞造)的防範;

    性能: 一次網絡往返時間(通過資料庫查詢session資訊)總比做一次HMACSHA256計算 的Token驗證和解析要費時得多;

    不需要為登入頁面做特殊處理: 如果你使用Protractor 做功能測試的時候,不再需要為登入頁面做特殊處理;

    基于标準化:你的API可以采用标準化的 JSON Web Token (JWT). 這個标準已經存在多個後端庫(.NET, Ruby,Java,Python, PHP)和多家公司的支援(如:Firebase,Google, Microsoft)。

2、 JWT實作認證授權

2.1 什麼是JWT

JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在使用者和伺服器之間傳遞安全可靠的資訊。在Java世界中通過JJWT實作JWT建立和驗證。

JWT令牌由三部分組成,每部分中間使用點(.)分隔,比如:aa.bb.cc

  • Header

    頭部包括令牌的類型(即JWT)及使用的雜湊演算法(如HMAC SHA256或RSA)

{
  "alg": "HS256",
  "typ": "JWT"
}
           

将上邊的内容使用Base64Url編碼,得到一個字元串就是JWT令牌的第一部分(aa)。

  • Payload

    第二部分是負載,内容也是一個json對象,它是存放有效資訊的地方,它可以存放jwt提供的現成字段,比如:iss(簽發者),exp(過期時間戳), sub(面向的使用者)等,也可自定義字段。此部分不建議存放敏感資訊,因為此部分可以解碼還原原始内容。最後将第二部分負載使用Base64Url編碼,得到一個字元串就是JWT令牌的第二部分(bb)。

{
  "sub": "1234567890",
  "name": "456",
  "admin": true
}
           
  • Signature

    第三部分是簽名,此部分用于防止jwt内容被篡改。這個部分使用base64url将前兩部分進行編碼,編碼後使用點(.)連接配接組成字元串,最後使用header中聲明簽名算法進行簽名。HMACSHA256(aa+ “.” +bb, secret)其中secret簽名所使用的密鑰或者說鹽。

2.2 JWT的快速入門

2.2.1 token的建立

(1)引入依賴

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.6.0</version>
</dependency>
           

(2)建立類JWTAuthApplicationTest,測試生成token

public static void createJwt() {
		JwtBuilder builder = Jwts.builder().setId("666").setSubject("君莫笑").setIssuedAt(new Date())
				.signWith(SignatureAlgorithm.HS256, "qqxhb");
		System.out.println(builder.compact());
	}
           

token:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlkJvojqvnrJEiLCJpYXQiOjE1NjU2MDA0NTR9.gnq6RX6FRj748f9voVPgc81wBuWRyvKl1gODMPqU9S8

           
2.2.2 token的解析

我們剛才已經建立了token ,在web應用中這個操作是由服務端進行然後發給用戶端,用戶端在下次向服務端發送請求時需要攜帶這個token(這就好像是拿着一張門票一樣),那服務端接到這個token 應該解析出token中的資訊(例如使用者id),根據這些資訊查詢資料庫傳回相應的結果。

public static void parseJwt(String token) {
		Claims claims = Jwts.parser().setSigningKey("qqxhb").parseClaimsJws(token).getBody();
		System.out.println("id:" + claims.getId());
		System.out.println("subject:" + claims.getSubject());
		System.out.println("IssuedAt:" + claims.getIssuedAt());
	}
           

parseResult:

id:666
subject:君莫笑
IssuedAt:Mon Aug 12 17:06:24 CST 2019
           
2.2.3 自定義claims

我們剛才的例子隻是存儲了id和subject兩個資訊,如果你想存儲更多的資訊(例如角色)可以定義自定義claims

(1) 修改createJwt,并存儲角色頭像内容

java

public static String createJwt() {
		long exp = System.currentTimeMillis() + 60 * 1000;// 過期時間一分鐘
		JwtBuilder builder = Jwts.builder().setId("666").setSubject("君莫笑").setIssuedAt(new Date())
				.signWith(SignatureAlgorithm.HS256, "qqxhb")
				.setExpiration(new Date(exp))
				.claim("role", "admin")
				.claim("logo", "logo.png");
		return builder.compact();
	}
           

(2) 修改parseJwt,擷取角色頭像内容

public static void parseJwt(String token) {
		Claims claims = Jwts.parser().setSigningKey("qqxhb").parseClaimsJws(token).getBody();
		System.out.println("id:" + claims.getId());
		System.out.println("subject:" + claims.getSubject());
		System.out.println("role:" + claims.get("role"));
		System.out.println("logo:" + claims.get("logo"));
		SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
		System.out.println("簽發時間:"+sdf.format(claims.getIssuedAt()));
        System.out.println("過期時間:"+sdf.format(claims.getExpiration()));
        System.out.println("目前時間:"+sdf.format(new Date()) );
	}
           

parseResult:

id:666
subject:君莫笑
role:admin
logo:logo.png
簽發時間:2019-08-12 05:15:48
過期時間:2019-08-12 05:16:48
目前時間:2019-08-12 05:15:49
           

2.3 抽取JWT工具類

import java.util.Date;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@ConfigurationProperties("jwt.config")
public class JwtUtil {
	private String key;
	private long ttl;

	public String getKey() {
		return key;
	}

	public void setKey(String key) {
		this.key = key;
	}

	public long getTtl() {
		return ttl;
	}

	public void setTtl(long ttl) {
		this.ttl = ttl;
	}

	/**
	 * 建立Token
	 */
	public String createJWT(String id, String subject, Map<String, Object> map) {
		long now = System.currentTimeMillis();
		long exp = now + ttl;
		JwtBuilder jwtBuilder = Jwts.builder().setId(id).setSubject(subject).setIssuedAt(new Date())
				.signWith(SignatureAlgorithm.HS256, key);
		for (Map.Entry<String, Object> entry : map.entrySet()) {
			jwtBuilder.claim(entry.getKey(), entry.getValue());
		}
		if (ttl > 0) {
			jwtBuilder.setExpiration(new Date(exp));
		}
		String token = jwtBuilder.compact();
		return token;
	}

	/*
	 * 解析JWT
	 * 
	 * @param token
	 * 
	 * @return
	 */
	public Claims parseJWT(String token) {
		Claims claims = null;
		try {
			claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
		} catch (Exception e) {
		}
		return claims;
	}
}
           

修改工程的application.yml, 添加配置

jwt:
 config:
    key: auth-jwt
    ttl: 360000
           

2.4 登入成功簽發token

(1)配置JwtUtil,交給spring容器管理。

@Bean
	public JwtUtil jwtUtil() {
		return new JwtUtil();
	}
           

(2)添加登入方法

@RequestMapping(value = "/login", method = RequestMethod.POST)
	public Result login(@RequestBody Map<String, String> loginMap) {
		String userName = loginMap.get("userName");
		String password = loginMap.get("password");
		User user = userService.findByUserName(userName);
		// 登入失敗
		if (user == null || !user.getPassword().equals(password)) {
			return new Result(ResultCode.MOBILEORPASSWORDERROR);
		} else {// 登入成功
			Map<String, Object> map = new HashMap<>();
			map.put("companyName", user.getCompany());
			String token = jwtUtil.createJWT(user.getId(), user.getUserName(), map);
			return new Result(ResultCode.SUCCESS, token);
		}
	}
           

2.5 擷取使用者資訊鑒權

需求:使用者登入成功之後,會發送一個新的請求到服務端,擷取使用者的詳細資訊。擷取使用者資訊的過程中必須登入才能,否則不能擷取。

前後端約定:前端請求微服務時需要添加頭資訊Authorization ,内容為Bearer+空格+token

(1)添加響應值對象

@Setter
@Getter
public class ProfileResult {
	private String userName;
	private String company;
	// 角色權限資訊
	private Map<String, Object> roles = new HashMap<>();

	public ProfileResult(User user) {
		this.userName = user.getUserName();
		this.company = user.getCompany();

		Set<String> menus = new HashSet<>();
		Set<String> points = new HashSet<>();
		Set<String> apis = new HashSet<>();
		//根據使用者去擷取角色權限資訊:菜單,按鈕,接口
		this.roles.put("menus", menus);
		this.roles.put("points", points);
		this.roles.put("apis", apis);
	}
}
           

(2)添加profile接口方法

/**
	 * 使用者登入成功之後,擷取使用者資訊 
	 */
	@RequestMapping(value = "/profile", method = RequestMethod.POST)
	public Result profile(HttpServletRequest request) throws Exception {

		String userid = claims.getId();
		// 擷取使用者資訊
		User user = userService.findById(userid);
		// 根據不同的使用者級别擷取使用者權限
		ProfileResult result = new ProfileResult(user);
		return new Result(ResultCode.SUCCESS, result);
	}
           

(3)驗證token

思路:從請求中擷取key為Authorization的token資訊,并使用jwt驗證,驗證成功後擷取隐藏資訊。提取公用Controller

import io.jsonwebtoken.Claims;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ModelAttribute;

import com.qqxhb.auth.jwt.utils.JwtUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class BaseController {

	protected HttpServletRequest request;
	protected HttpServletResponse response;
	protected String company;
	protected Claims claims;

	@Autowired
	JwtUtil jwtUtil;

	@ModelAttribute
	public void setResAnReq(HttpServletRequest request, HttpServletResponse response) {
		this.request = request;
		this.response = response;
		String authorization = request.getHeader("Authorization");
		if (StringUtils.isEmpty(authorization)) {
			return;
		}
		// 前後端約定頭資訊内容以 Bearer+空格+token 形式組成
		String token = authorization.replace("Bearer ", "");
		// 比較并擷取claims
		Claims claims = jwtUtil.parseJWT(token);
		Object obj = request.getAttribute("user_claims");

		if (obj != null) {
			this.claims = (Claims) obj;
			this.company = (String) claims.get("company");
		}
	}

}

           

2.6 通過攔截器鑒權

@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {

    /**
     * 簡化擷取token資料的代碼編寫(判斷是否登入)
     *  1.通過request擷取請求token資訊
     *  2.從token中解析擷取claims
     *  3.将claims綁定到request域中
     */

    @Autowired
    private JwtUtil jwtUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.通過request擷取請求token資訊
        String authorization = request.getHeader("Authorization");
        //判斷請求頭資訊是否為空,或者是否已Bearer開頭
        if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")) {
            //擷取token資料
            String token = authorization.replace("Bearer ","");
            //解析token擷取claims
            Claims claims = jwtUtils.parseJWT(token);
            if(claims != null) {
                //通過claims擷取到目前使用者的可通路API權限字元串
                String apis = (String) claims.get("apis");  //api-user-delete,api-user-update
                //通過handler
                HandlerMethod h = (HandlerMethod) handler;
                //擷取接口上的reqeustmapping注解
                RequestMapping annotation = h.getMethodAnnotation(RequestMapping.class);
                //擷取目前請求接口中的name屬性
                String name = annotation.name();
                //判斷目前使用者是否具有響應的請求權限
                if(apis.contains(name)) {
                    request.setAttribute("user_claims",claims);
                    return true;
                }else {
                    throw new CommonException(ResultCode.UNAUTHORISE);
                }
            }
        }
        throw new CommonException(ResultCode.UNAUTHENTICATED);
    }
}
           
注冊配置攔截器
@Configuration
public class JwtMvcConfig extends WebMvcConfigurationSupport {

    @Autowired
    private JwtInterceptor jwtInterceptor;

    /**
     * 添加JWT鑒權攔截器
     */
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor).
                addPathPatterns("/**").//指定攔截器的url位址
                excludePathPatterns("/login","/register/**");//指定不攔截的url位址
    }
}
           
修改BaseController
@Component
public class BaseController {

	protected HttpServletRequest request;
	protected HttpServletResponse response;
	protected String company;
	protected Claims claims;

	@ModelAttribute
	public void setResAnReq(HttpServletRequest request, HttpServletResponse response) {
		this.request = request;
		this.response = response;
		Object obj = request.getAttribute("user_claims");

		if (obj != null) {
			this.claims = (Claims) obj;
			this.company = (String) claims.get("company");
		}
	}

}
           

3、Shiro安全架構實作認證授權

3.1 什麼是Shiro

3.1.1 什麼是Shiro

Apache Shiro是一個強大且易用的Java安全架構,執行身份驗證、授權、密碼和會話管理。使用Shiro的易于了解的API,您可以快速、輕松地獲得任何應用程式,從最小的移動應用程式到最大的網絡和企業應用程式。

Apache Shiro 的首要目标是易于使用和了解。安全有時候是很複雜的,甚至是痛苦的,但它沒有必要這樣。架構應該盡可能掩蓋複雜的地方,露出一個幹淨而直覺的 API,來簡化開發人員在使他們的應用程式安全上的努力。以下是你可以用 Apache Shiro 所做的事情:

  • 驗證使用者來核實他們的身份
  • 對使用者執行通路控制,如:判斷使用者是否被配置設定了一個确定的安全角色;判斷使用者是否被允許做某事
  • 在任何環境下使用 Session API,即使沒有 Web 或 EJB 容器。
  • 在身份驗證,通路控制期間或在會話的生命周期,對事件作出反應。
  • 聚集一個或多個使用者安全資料的資料源,并作為一個單一的複合使用者“視圖”。
  • 啟用單點登入(SSO)功能。
  • 為沒有關聯到登入的使用者啟用"Remember Me"服務
3.1.2 與Spring Security的對比

Spring Security:

除了不能脫離Spring,shiro的功能它都有。而且Spring Security對Oauth、OpenID也有支援,Shiro則需要自己手動實作。Spring Security的權限細粒度更高。

Shiro:

Shiro較之 Spring Security,Shiro在保持強大功能的同時,還在簡單性和靈活性方面擁有巨大優勢。

  1. 易于了解的 Java Security API;
  2. 簡單的身份認證(登入),支援多種資料源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
  3. 對角色的簡單的簽權(通路控制),支援細粒度的簽權;
  4. 支援一級緩存,以提升應用程式的性能;
  5. 内置的基于 POJO 企業會話管理,适用于 Web 以及非 Web 的環境;
  6. 異構用戶端會話通路;
  7. 非常簡單的加密 API;
  8. 不跟任何的架構或者容器捆綁,可以獨立運作
3.1.3 Shiro的功能子產品

Shiro可以非常容易的開發出足夠好的應用,其不僅可以用在JavaSE環境,也可以用在JavaEE環境。Shiro可以幫助我們完成:認證、授權、加密、會話管理、與Web內建、緩存等。而且Shiro的API也是非常簡單;其基本功能點如下圖所示:

認證授權示例——JWT及Shiro

Authentication:身份認證/登入,驗證使用者是不是擁有相應的身份。

Authorization:授權,即權限驗證,驗證某個已認證的使用者是否擁有某個權限;即判斷使用者是否能做事情。

Session Management:會話管理,即使用者登入後就是一次會話,在沒有退出之前,它的所有資訊都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的。

Cryptography:加密,保護資料的安全性,如密碼加密存儲到資料庫,而不是明文存儲。

Web Support:Shiro 的 web 支援的 API 能夠輕松地幫助保護 Web 應用程式。

Caching:緩存,比如使用者登入後,其使用者資訊、擁有的角色/權限不必每次去查,這樣可以提高效率。

Concurrency:Apache Shiro 利用它的并發特性來支援多線程應用程式。

Testing:測試支援的存在來幫助你編寫單元測試和內建測試,并確定你的能夠如預期的一樣安全。

“Run As”:一個允許使用者假設為另一個使用者身份(如果允許)的功能,有時候在管理腳本很有用。

“Remember Me”:記住我。

3.2 Shiro的内部結構

認證授權示例——JWT及Shiro

Subject:主體,可以看到主體可以是任何可以與應用互動的“使用者”;

SecurityManager:相當于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心髒;所有具體的互動都通過SecurityManager進行控制;它管理着所有Subject、且負責進行認證和授權、及會話、緩存的管理。

Authenticator:認證器,負責主體認證的,這是一個擴充點,如果使用者覺得Shiro預設的不好,可以自定義實作;其需要認證政策(Authentication Strategy),即什麼情況下算使用者認證通過了;

Authrizer:授權器,或者通路控制器,用來決定主體是否有權限進行相應的操作;即控制着使用者能通路應用中的哪些功能;

Realm:可以有1個或多個Realm,可以認為是安全實體資料源,即用于擷取安全實體的;可以是JDBC實作,也可以是LDAP實作,或者記憶體實作等等;由使用者提供;注意:Shiro不知道你的使用者/權限存儲在哪及以何種格式存儲;是以我們一般在應用中都需要實作自己的Realm;

SessionManager:如果寫過Servlet就應該知道Session的概念,Session呢需要有人去管理它的生命周期,這個元件就是SessionManager;而Shiro并不僅僅可以用在Web環境,也可以用在如普通的JavaSE環境、EJB等環境;所有呢,Shiro就抽象了一個自己的Session來管理主體與應用之間互動的資料;

SessionDAO:DAO大家都用過,資料通路對象,用于會話的CRUD,比如我們想把Session儲存到資料庫,那麼可以實作自己的SessionDAO,通過如JDBC寫到資料庫;比如想把Session放到Memcached中,可以實作自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache進行緩存,以提高性能;

CacheManager:緩存控制器,來管理如使用者、角色、權限等的緩存的;因為這些資料基本上很少去改變,放到緩存中後可以提高通路的性能

Cryptography:密碼子產品,Shiro提高了一些常見的加密元件用于如密碼加密/解密的。

3.3 應用程式使用Shiro

認證授權示例——JWT及Shiro

也就是說對于我們而言,最簡單的一個Shiro應用:

1、應用代碼通過Subject來進行認證和授權,而Subject又委托給SecurityManager;

2、我們需要給Shiro的SecurityManager注入Realm,進而讓SecurityManager能得到合法的使用者及其權限進行判斷。從以上也可以看出,Shiro不提供維護使用者/權限,而是通過Realm讓開發人員自己注入。

3.4 Shiro的入門

3.4.1 搭建基于ini的運作環境

(1)建立工程導入shiro坐标

<dependencies>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.3.2</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
           
3.4.2 使用者認證

認證:身份認證/登入,驗證使用者是不是擁有相應的身份。基于shiro的認證,是通過subject的login方法完成使用者認證工作的

(1)在resource目錄下建立shiro的ini配置檔案構造模拟資料(shiro-auth.ini)

[users]
#模拟從資料庫查詢的使用者
#資料格式  使用者名=密碼
zhangsan=123456
lisi=654321
           

(2)測試使用者認證

@Test
	public void testLogin() {
		// 1.根據配置檔案建立SecurityManagerFactory
		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-1.ini");
		// 2.通過工廠擷取SecurityManager
		SecurityManager securityManager = factory.getInstance();
		// 3.将SecurityManager綁定到目前運作環境
		SecurityUtils.setSecurityManager(securityManager);
		// 4.從目前運作環境中構造subject
		Subject subject = SecurityUtils.getSubject();
		// 5.構造shiro登入的資料
		String username = "zhangsan";
		String password = "1234561";
		UsernamePasswordToken token = new UsernamePasswordToken(username, password);
		// 6.主體登陸
		subject.login(token);
		// 7.驗證使用者是否登入成功
		System.out.println("使用者是否登入成功=" + subject.isAuthenticated());
		// 8.擷取登入成功的資料
		System.out.println(subject.getPrincipal());
	}
           
3.4.3 使用者授權

授權,即權限驗證,驗證某個已認證的使用者是否擁有某個權限;即判斷使用者是否能做事情,常見的如:驗證某個使用者是否擁有某個角色。或者細粒度的驗證某個使用者對某個資源是否具有某個權限

(1)在resource目錄下建立shiro的ini配置檔案構造模拟資料(shiro-prem.ini)

[users]
#模拟從資料庫查詢的使用者
#資料格式  使用者名=密碼,角色1,角色2..
zhangsan=123456,role1,role2
lisi=654321,role2
[roles]
#模拟從資料庫查詢的角色和權限清單
#資料格式  角色名=權限1,權限2
role1=user:save,user:update
role2=user:update,user.delete
role3=user.find
           

(2)完成使用者授權

private SecurityManager securityManager;

    @Before
    public void init() {
        //1.根據配置檔案建立SecurityManagerFactory
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-2.ini");
        //2.通過工廠擷取SecurityManager
        SecurityManager securityManager = factory.getInstance();
        //3.将SecurityManager綁定到目前運作環境
        SecurityUtils.setSecurityManager(securityManager);
    }

    @Test
    public void testLogin() {
        Subject subject = SecurityUtils.getSubject();
        String username = "lisi";
        String password = "123456";
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        subject.login(token);

        //登入成功之後,完成授權
        //授權:檢驗目前登入使用者是否具有操作權限,是否具有某個角色
        System.out.println(subject.hasRole("role1"));
        System.out.println(subject.isPermitted("user:save"));

    }
           
3.4.4 自定義Realm

Realm域:Shiro從Realm擷取安全資料(如使用者、角色、權限),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm擷取相應的使用者進行比較以确定使用者身份是否合法;也需要從Realm得到使用者相應的角色/權限進行驗證使用者是否能進行操作;可以把Realm看成DataSource,即安全資料源

(1)自定義Realm

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 java.util.ArrayList;
import java.util.List;

/**
 * 自定義realms對象 繼承AuthorizingRealm 重寫方法 doGetAuthorizationInfo:授權
 * 擷取到使用者的授權資料(使用者的權限資料) doGetAuthenticationInfo:認證 根據使用者名密碼登入,将使用者資料儲存(安全資料)
 *
 */
public class PermissionRealm extends AuthorizingRealm {

	/**
	 * 自定義realm名稱
	 */
	public void setName(String name) {
		super.setName("permissionRealm");
	}

	// 授權:授權的主要目的就是根據認證資料擷取到使用者的權限資訊

	/**
	 * principalCollection:包含了所有已認證的安全資料 AuthorizationInfoInfo:授權資料
	 */
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
		System.out.println("執行授權方法");
		// 1.擷取安全資料 username,使用者id
		String username = (String) principalCollection.getPrimaryPrincipal();
		// 2.根據id或者名稱查詢使用者
		// 3.查詢使用者的角色和權限資訊
		List<String> perms = new ArrayList<>();
		perms.add("user:save");
		perms.add("user:update");
		List<String> roles = new ArrayList<>();
		roles.add("role1");
		roles.add("role2");
		// 4.構造傳回
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		// 設定權限集合
		info.addStringPermissions(perms);
		// 設定角色集合
		info.addRoles(roles);
		return info;
	}

	// 認證:認證的主要目的,比較使用者名和密碼是否與資料庫中的一緻
	// 将安全資料存入到shiro進行保管
	// 參數:authenticationToken登入構造的usernamepasswordtoken
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
			throws AuthenticationException {
		System.out.println("執行認證方法");
		// 1.構造uptoken
		UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
		// 2.擷取輸入的使用者名密碼
		String username = upToken.getUsername();
		String password = new String(upToken.getPassword());
		// 3.根據使用者名查詢資料庫,正式系統查詢
		// 4.比較密碼和資料庫中的密碼是否一緻(密碼可能需要加密)
		if ("123456".equals(password)) {
			// 5.如果成功,向shiro存入安全資料
			SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());// 1.安全資料,2.密碼。3。目前realm域名稱
			return info;
		} else {
			// 6.失敗,抛出異常或傳回null
			throw new RuntimeException("使用者名或密碼錯誤");
		}
	}
}

           

(2)配置shiro的ini配置檔案(shiro-realm.ini)

[main]
#聲明realm
permReam=com.qqxhb.auth.shiro.PermissionRealm
#注冊realm到securityManager中
securityManager.realms=$permReam
           

(3)驗證

@Before
	public void init() {
		// 1.根據配置檔案建立SecurityManagerFactory
		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-3.ini");
		// 2.通過工廠擷取SecurityManager
		SecurityManager securityManager = factory.getInstance();
		// 3.将SecurityManager綁定到目前運作環境
		SecurityUtils.setSecurityManager(securityManager);
	}
	
@Test
	public void testLogin3() {
		Subject subject = SecurityUtils.getSubject();
		String username = "zhangsan";
		String password = "123456";
		UsernamePasswordToken token = new UsernamePasswordToken(username, password);

		// 執行login-->realm域中的認證方法
		subject.login(token);

		// 授權:-->realm域中的授權方法
		System.out.println(subject.hasRole("role1"));
		System.out.println(subject.isPermitted("user:save"));

	}
           
3.4.5 認證與授權的執行流程分析

(1)認證流程

認證授權示例——JWT及Shiro
  1. 首先調用Subject.login(token)進行登入,其會自動委托給Security Manager,調用之前必須通過SecurityUtils.setSecurityManager()設定;
  2. SecurityManager負責真正的身份驗證邏輯;它會委托給Authenticator進行身份驗證;
  3. Authenticator才是真正的身份驗證者,Shiro API中核心的身份認證入口點,此處可以自定義插入自己的實作;
  4. Authenticator可能會委托給相應的AuthenticationStrategy進行多Realm身份驗證,預設ModularRealmAuthenticator會調用AuthenticationStrategy進行多Realm身份驗證;
  5. Authenticator會把相應的token傳入Realm,從Realm擷取身份驗證資訊,如果沒有傳回/抛出異常表示身份驗證失敗了。此處可以配置多個Realm,将按照相應的順序及政策進行通路。

(2)授權流程

認證授權示例——JWT及Shiro

6. 首先調用Subject.isPermitted/hasRole接口,其會委托給SecurityManager,而SecurityManager接着會委托給Authorizer;

7. Authorizer是真正的授權者,如果我們調用如isPermitted(“user:view”),其首先會通過PermissionResolver把字元串轉換成相應的Permission執行個體;

8. 在進行授權之前,其會調用相應的Realm擷取Subject相應的角色/權限用于比對傳入的角色/權限;

9. Authorizer會判斷Realm的角色/權限是否和傳入的比對,如果有多個Realm,會委托給ModularRealmAuthorizer進行循環判斷,如果比對如isPermitted/hasRole會傳回true,否則傳回false表示授權失敗。

源碼位址:https://github.com/qqxhb/auth-demo

篇幅已經很長,shiro整合springboot見下一篇。