Shiro是Apache下的一個安全架構,其相比Spring Security來說,更為輕量級,而功能卻不簡單。相關對比目前有很多文章都提到。但尚未有真正技術性的文章——僅有的幾篇也隻有介紹介紹如何配置成功一個應用而已,其實這個還是看官方文檔更清晰。
但官方文檔并沒有很明确地指出如何實作Shiro的單點登入,是以我覺得有必要在此處記錄一下,友善使用shiro的朋友們。
shiro支援幾乎所有的登入方式——因為它的靈活性和可定制性,是以我所提供的方案也隻是其中的一種,大家如果有别的想法,自由定制之。
首先,要實作單點登入,必須有sso服務,假設該服務已部署完成,這裡我用的是jasig cas進行單點登入驗證,其機理大緻就是在應用中增加一層filter進行攔截請求,如果發現request無認證資訊(用戶端驗證憑據)則由filter直接發送302重定向至cas認證伺服器,使用者認證成功後會帶着成功的唯一憑據再次進入應用,此時該filter将根據用戶端提供的驗證憑據連接配接到cas伺服器擷取使用者資訊,通過s2s擷取到使用者資訊後放入應用中完成使用者對本應用的授權。
那麼在shiro中如何去配合cas進行sso呢?接下來我們就來對shiro進行sso配置
1、建一個自定義的token
[java] view plain copy
- package com.jajacode.sample.authc
- import org.apache.shiro.authc.*
- public class TrustedSsoAuthenticationToken implements AuthenticationToken{
- private String username;
- public TrustedSsoAuthenticationToken(){}
- public TrustedSsoAuthenticationToken(String username){
- this.username = username;
- }
- public Object getPrincipal(){
- return this.username;
- }
- public Object getCredentials() {
- return null;
- }
- public void setUsername(String username){
- this.username = username;
- }
- public String getUsername(){
- return this.username;
- }
- public void clear(){
- this.username = null;
- }
- public String toString(){
- return "username="+this.username;
- }
- }
2、建立一個filter,這個filter就要根據實際需要進行extends了,因為我用了jasig cas,是以代碼會是如下:
[java] view plain copy
- package com.jajacode.sample.filter.authc
- import java.io.IOException;
- import java.security.Principal;
- import javax.servlet.Filter;
- import javax.servlet.FilterChain;
- import javax.servlet.FilterConfig;
- import javax.servlet.ServletException;
- import javax.servlet.ServletRequest;
- import javax.servlet.ServletResponse;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.servlet.http.HttpSession;
- import org.apache.shiro.SecurityUtils;
- import com.jajacode.sample.authc.TrustedSsoAuthenticationToken;
- import com.jajacode.sample.datasource.DatasourceContextHolder;
- public class ShiroSsoFilter implements Filter {
- public static final String CONST_CAS_ASSERTION = "_const_cas_assertion_";
- @Override
- public void destroy() {
- // TODO Auto-generated method stub
- }
- @Override
- public void doFilter(final ServletRequest servletRequest,
- final ServletResponse servletResponse, final FilterChain filterChain)
- throws IOException, ServletException {
- final HttpServletRequest request = (HttpServletRequest) servletRequest;
- final HttpServletResponse response = (HttpServletResponse) servletResponse;
- final HttpSession session = request.getSession();
- Principal principal = request.getUserPrincipal();
- if (principal != null) {
- // 這裡是多源資料庫的選擇,系統根據使用者組的不同會選擇不同的資料庫操作
- DatasourceContextHolder.setGroupType(GroupType.CUSTOMER);
- TrustedSsoAuthenticationToken token = new TrustedSsoAuthenticationToken(principal.getName());
- SecurityUtils.getSubject().login(token);
- filterChain.doFilter(request, response);
- }
- }
- @Override
- public void init(FilterConfig arg0) throws ServletException {
- // TODO Auto-generated method stub
- }
- }
3、建立sso的realm,此realm繼承shiro的AuthorizingRealm,并重寫doGetAuthenticationInfo方法
[java] view plain copy
- package com.jajacode.sample.realm;
- import java.util.Collection;
- import java.util.HashSet;
- import org.apache.shiro.authc.AccountException;
- import org.apache.shiro.authc.AuthenticationException;
- import org.apache.shiro.authc.AuthenticationInfo;
- import org.apache.shiro.authc.AuthenticationToken;
- import org.apache.shiro.authc.SimpleAuthenticationInfo;
- import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
- import org.apache.shiro.authz.AuthorizationInfo;
- import org.apache.shiro.authz.SimpleAuthorizationInfo;
- import org.apache.shiro.cache.Cache;
- import org.apache.shiro.realm.AuthorizingRealm;
- import org.apache.shiro.subject.PrincipalCollection;
- import org.apache.shiro.subject.SimplePrincipalCollection;
- import org.springframework.beans.factory.annotation.Autowired;
- import com.jajacode.sample.authc.TrustedSsoAuthenticationToken;
- import com.jajacode.sample.domain.account.Permission;
- import com.jajacode.sample.domain.account.Role;
- import com.jajacode.sample.domain.account.User;
- public class ShiroSsoRealm extends AuthorizingRealm {
- @Autowired
- private AccountManager accountManager;
- public ShiroDbRealm(){
- // 設定無需憑證,因為從sso認證後才會有使用者名
- setCredentialsMatcher(new AllowAllCredentialsMatcher());
- // 設定token為我們自定義的
- setAuthenticationTokenClass(TrustedSsoAuthenticationToken.class);
- }
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(
- AuthenticationToken authcToken) throws AuthenticationException {
- TrustedSsoAuthenticationToken token = (TrustedSsoAuthenticationToken)authcToken;
- //UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
- Object username = token.getPrincipal();
- // String username = token.getUsername();
- //不允許無username
- if(username==null){
- // 自定義異常,于前端捕獲
- throw new AccountException("使用者名不允許為空!");
- }
- return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName());
- }
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- String loginName = (String) principals.fromRealm(getName()).iterator().next();
- User user = accountManager.findUserByLoginName(loginName);
- if(user != null){
- SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
- // 将使用者權限放入其中,代碼略
- return info;
- }
- return null;
- }
- public void clearCachedAuthorizationInfo(String principal){
- SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName());
- clearCachedAuthorizationInfo(principals);
- }
- public void clearAllCachedAuthorizationInfo(){
- Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
- if (cache != null) {
- for (Object key : cache.keys()) {
- cache.remove(key);
- }
- }
- }
- }
4、其他配置參考官方正常配置即可。将filter寫入web.xml中,同時配置sso的一些filter注意mapping順序即可。
歡迎拍磚
其實按照shiro标準,後面的Filter應該繼承自org.apache.shiro.web.filter.authc.AuthenticatingFilter會好一些,并且重寫方法:createToken(request,response),傳回TrustedSsoAuthenticationToken執行個體;重寫onAccessDenied(request,response)方法,來調用executeLogin(request,response),最終還是調用了SecurityUtils.getSubject().login(token),是以我就簡化到直接使用filter來實作,單例的Subject非常友善!