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
- 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,所以代码会是如下:
- 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方法
- 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非常方便!