分享

Apache Shiro实现单点登录SSO | 沐风

 richsky 2012-04-15

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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,所以代码会是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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 {

  /**
   * Represents the constant for where the assertion will be located in
   * memory.
   */

  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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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;

/**
 * 安全认证最主要的实现类
 * @author Yockii Hsu
 *
 */

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;
  }
 
  /**
   * 清空用户关联权限认证,待下次使用时重新加载。
   * @param principal
   */

  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非常方便!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多