【shiro】从零开始认识 shiro

<  最近刚接触使用 shiro 这里简单记录一下过程 。 系统学习可以参看 : 官网  Apache Shiro Reference Documention >

1.  简单介绍 


       ★    Apache Shiro 是 一款强大的 容易使用的 Java  安全框架,其主要功能如下 :

                       Authentication (认证)、                    Authorization (授权)、            Cryptography(加密) 、

                       Session Management (会话管理)、 Web Integration(Web 集成) 、Integrations(各种集成)


2.    shiro 总览

                 ★    shiro 的 特色


                 ★    Shiro 基础架构


 详细 :

        

                  ①     Subject  : 主题 ,


                     代表着程序使用者的一个 “视图” ,代表着 正在使用该程序的任意实体(不仅仅是人,可以是其他的外部调用)。简单点说就是包含了登入者信息的


一个 JavaBean。登入者信息包括  实体的状态以及一些安全操作,操作包括  authenticaion 认证 (login /logout), authorization  授权 (access controll) 以及  session access .


         Subject 及其 子类关系类图如下  (详细可查看官方提供的 API):

 


         Subject 对象常用获取方式 如下:

// 使用 shiro 提供的 SecurityUtils类,可以用于获取 Subject 以及设置/获取 SecurityManager

          Subject user = SecurityUtils.getSubject();

        Subject 对象 常用方法  :


void login(AuthenticationToken token) throws AuthenticationException  // 使用token 进行登录 ,接下来会调用 realm

void logout();     //登出 , session 终止

Object getPrincipal();  //  获取登录的身份验证(如果是用户名密码登录的话,获取到用户名)

//  权限相关
boolean hasRole(String roleIdentifier); //  验证 角色

boolean isAuthenticated();  // 验证是否 认证

boolean isRemembered(); //  是否记住用户名密码

void checkRole(String roleIdentifier) throws AuthorizationException  //  断言 判断 角色  ,成功则继续执行,否则抛出异常
Session getSession();  //获取session ,如果不存在则会创建

Session getSession(boolean create); // 如果存在 session 则返回session ; 
                                   // 如果不存在 session 并且  create 为 true,则创建并返回,如果不存在且 create 为 false 则返回 null

   


                   ②    SecurityManager :      负责管理 所有的 Subject , 就像一个保护伞在默默保护 着 Subject 的安全操作 。通常我们配置好管理之后就不用管 该对象,而主要关

注 Subject   API .


        SecurityManager 及其子类 类图结构如下 :


                    



                    ③   realm  : 领域(字面意思)


               用于 访问 应用程序 的安全数据 ,例如  用户(帐号密码等)  ,角色  ,权限 ,以达到认证和授权的目的 , 可以认为 realm 是一个特殊的  DAO   。我们一般会选择继承 realm 的 抽象类  AuthenticatingRealm  或者  AuthorizingRealm  来自定义 Realm.  如果不希望该 realm  执行 认证, 则覆盖 supports (AuthenticationToken)方法,并且返回 false 即可。或者 如果仅仅需要指定的 token 执行该 realm 进行认证 ,可以 使用 instanceof  限制 token 种类 ,如下栗子只希望 ExternalToken(自定义 token) 认证该 realm:

 

 @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof ExternalToken;
    }

     shiro 默认提供 realm类图如下 :

                 顶级父类 Realm 的 方法

String getName();

boolean supports(AuthenticationToken token);  //  如果返回 true 则 希望通过给定的 AuthenticationToken 实例 认证 Subject ;否则不认证

AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;  //  返回用户 登录的账户信息(principals、credentials)

            Realm 子类介绍
CachingRealm  :  Realm 的 抽象类 ,为  子类 提供  缓存支持   ,可以手动开启/关闭

AuthenticatingRealm : Realm很重要的一个抽象类 ,该抽象类仅仅 提供 认证 支持,授权由其 子类实现;也就是说该类只有一个抽象方法其子类必须实现:
                      protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException :
                                     AuthenticationToken  用户登录信息的重整,子类:UsernamePasswordToken 等;

AuthorizingRealm : AuthenticatingRealm的抽象类,增加了 授权支持 ;抽象方法:
               protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals):
                            PrincipalCollection 是所有 principals 的集合,principal 是登录时身份的验证,如用户名,id等 :获得用户名的方法 
                                       fromRealm(getName()).iterator().next();

CasRealm : AuthorizingRealm 的实现类 ;作为一个  CAS 的客户端 与 CAS 的服务器端完成认证和基本的授权 。当 CasToken 提交时 会进行检查该 Realm


                 ★    Shiro 架构底层细节




详细  :

                 ①  Authenticator  :    验证器 

                  Authenticator  负责 执行和应对  用户的认证( login)操作 , Shiro Api 的 切入点之一 , 用户一般不需要直接和 Authenticator 打交道 : 因为 Shiro 的默认架构是基于全局的并且通常已经 集成了 Authenticator 实例 的 SecurityManager . 

             

                    Authenticator 的相关实现类图如下


                      Authenticator 只有一个抽象方法

 public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws AuthenticationException;



                    ②  AuthenticationStrategy : 认证策略

           在 PAM(pluggable realm) 环境中 通过配置认证策略来 协助 Authenticator 完成  多个 Realm  的认证 。

         策略种类  :  ★  所有的 Realm 都要 认证成功

                               ★ 至少一个 Realm 认证成功   (默认)

                               ★ 第一个 Realm  认证成功


             相关实现类图如下



                           ③ Authorizer  : 授权者

                        负责已登入 Subject 的 鉴权操作(access control);

              相关类图如下所示


                           ④ SessionManager  :  会话管理

                    可以在任何环境下 使用 Session  ,不仅仅是 Servlet 容器中或者 EJB 容器中 。

      相关类图如下 :


3.  涉及到的主要的概念或者主要对象


         ①  Subject  : 主题 ,

 

         ②   Realm :     负责访问 安全数据,相当于 Dao 层 ,负责连接 shiro 和 应用安全数据的 桥梁 ,在用户 登录 (login)时 进行 认证 ,在访问时进行鉴权 (access control)。 SecurityManager 负责管理 Realm ,

          ③  Authentication  : 认证

                 

                     验证 登录的  Subject 的 身份 ,一旦验证 成功 ,则 登入的 Subject 就会被信任,isAuthenticated 属性被设置为 true 。 一般   Realm 的实现类 会进行 认证这一环节

          关于认证  相关:

         ④  Authorization:  授权 , 鉴权

              

                      即 access control  (访问控制)  ,  确定  登入者 (Subject / User)  是否有权限 。 通常是 通过 Subject 的 roles / permissions  来 允许或 拒绝 资源的请求 , 一

般 Realm 的实现类 会进行 授权这一环节 。

        ⑤  roles :   角色

                 基于角色的权限控制  ( Role-Based  Access  Control) ,即把权限(permission)分配给角色 (role),然后 再把角色 分配给 用户(Subject)。

        ⑥  permission:  权限

                    细颗粒度的控制,决定什么操作可以做,什么操作不可以做 。

        ⑦  Principal

                     应用登入者的身份属性(通常是 username / userId)

        ⑧ Credenial  : 凭据

                    应用登入者的凭据 (通常是 password)

        ⑨ Session :  回话管理

                    登入之后便存在会话 , 和 HttpSession 有相同 的 目的 。但不同的是该 Session 既可以在 Servlet 容器中 也可以在 EJB 容器中 使用 。一旦登出 或 session 超时

时, session 即 终止 。

4.  浅析 登入时的验证流程

       

           ★  登录时的认证  调用类流程图如下 :

                    ★ 登录认证示例代码

controller 手动调用 Subject 的 login 方法 :

@RequestMapping(value="/loginTo",method = RequestMethod.POST)
public String login(@RequestParam("username")String username,@RequestParam("password")String password,Model m){
                UsernamePasswordToken token = new UsernamePasswordToen(username,password); //手动构造token
                Subject currentUser = SecurityUtils.getSubject(); //获取 Subject 对象
		try {
			currentUser.login(token); //调用 login 方法 认证
		} catch (UnknownAccountException uae) {
			m.addAttribute("message", "没有找到用户信息,请确认警号输入是否正确。");
			return "login";
		} catch (IncorrectCredentialsException ice) {
			m.addAttribute("message", "密码与帐号不匹配,请确认是否输入了正确的密码。注意键盘是否在大写输入状态。");
			return "login";
		} catch (AuthenticationException ae) {
			m.addAttribute("message", "登录信息不正确,请联系管理员。");
			return "login";
		} catch (Exception e) {
			m.addAttribute("message", "服务内部错误,请联系管理员。");
			return "login";  //返回登录页面
		}

		return "home"; //登录后的首页
}


         ★ 自定义 Realm 获取用户数据并验证

              

                   AuthenticationToken  和  AuthenticationInfo  的区别  :

                  这两个 方法基本上相同,只是返回参数不同 ,本质区别在于  :   AutenticationInfo  存储的账户信息 已经被 认证  

 AuthenticationToken 存储的账户信息是尝试登录的用户信息 ,该信息还有待认证是否通过 。

                  AuthenticationToken 相关类图 如下,通常使用 实现类 UsernamePasswordToken,如果这些 均不能满足需求,可以自定义 token (实现 AuthenticationToken) :


                 AuthenticationInfo 相关类图如下 ,通常使用实现类 SimpleAuthenticationInfo 即可满足一般的需求:

                 接着便会调用 自定义的 Realm 的  doGetAuthenticationInfo () 方法进行 认证  ,大致代码如下,通过即代表登录成功:

public class LoginRealm extends AuthorizingRealm {
	@Autowired
	private AccountService accountService;

	/**
	 * 实现用户的授权
	 * @param principalCollection
	 * @return
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(
			PrincipalCollection principalCollection) {
		String loginId = (String)principalCollection.getPrimaryPrincipal();
		User user = accountService.findById(loginId);
		if (null == user) {
			return null;
		} else {
			SimpleAuthorizationInfo result = new SimpleAuthorizationInfo();
			String roleName = accountService.getRoleName(user);
			result.addRole(roleName);
			result.addStringPermission("view");
			return result;
		}
	}

	/**
	 * 实现用户认证
	 * @param token
	 * @return
	 * @throws AuthenticationException
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken token) throws AuthenticationException {
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;

		User user = accountService.findById(upToken.getUsername());

		if (user != null && user.getDisable() == 0) {
			return new SimpleAuthenticationInfo(user.getId(),
					user.getPassword(), getName());
		}
		return null;
	}

}

                  如果外部调用内部接口该如何实现认证呢?

    外部调用内部接口需要如下操作:

            ① 将需要被调用的接口的 URL 形式 与 不需要被调用的接口的 URL 形式区分开来,在权限配置文件中设置需要被调用的接口的 URL 不需要认证,即 anon,并且增加一个过滤器,过滤该类 URL 。

如下  auth.properties 配置文件 :

...
/api/** = api, perms[view]   //内部使用的接口过滤器 , api 为自定义过滤器
/public/**= anon,external    //可供外部调用的接口的URL过滤器 , external 为自定义过滤器
....


当然配置文件以及下面的 filter 需要在启动时交给 spring 管理 ,

AppConfig  部分代码 (基于 java 配置文件)


	@Bean(name="api")
	public Filter apiFilter() {
		return new ApiFilter();
	}

	@Bean(name = "external")
	public ExternalCallFilter externalCallFilter(){
		return new ExternalCallFilter();
	}
	@Bean(name="shiroFilter")
	public ShiroFilterFactoryBean shiroFilter() throws IOException {
		Resource resource = new ClassPathResource("auth.properties");
		String authConfig = FileCopyUtils.copyToString(new FileReader(resource.getFile()));

		Map<String, Filter> filters = new HashMap<String, Filter>();
		filters.put("CASFilter", casFilter());

		ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
		shiroFilter.setSecurityManager(securityManager());
		shiroFilter.setLoginUrl("xxxxxx");
		//		shiroFilter.setLoginUrl("login");
		shiroFilter.setSuccessUrl("/");
		shiroFilter.setFilters(filters);
		shiroFilter.setUnauthorizedUrl("/unauthorized");
		shiroFilter.setFilterChainDefinitions(authConfig);
		return shiroFilter;
        }
	/**
	 * securityManager 对 Subject 对象进行管理
	 *          Subject 一般来说代表当前登录的用户
	 * @return
	 */
	@Bean
	public WebSecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		ArrayList<Realm> realms = new ArrayList<Realm>();
		realms.add(casrealm());
		realms.add(realm());
		realms.add(this.externalRealm());
		securityManager.setRealms(realms);
		return securityManager;
	}
	@Bean
	public Realm realm() {
		PpsRealm realm = new PpsRealm();
		realm.setCredentialsMatcher(new HashedCredentialsMatcher("MD5"));
		return realm;
	}

	@Bean
	public Realm casrealm() {
		CASRealm realm = new CASRealm();
		realm.setCasServerUrlPrefix("xxxxx");
		realm.setCasService("xxxxx");
		return realm;
	}

	@Bean
	public Realm externalRealm(){
		ExternalReam externalReam = new ExternalReam();
		return externalReam;
	}




             ②  api 过滤器 和  external 过滤器

ApiFilter 过滤器 :

public final class ApiFilter extends UserFilter {

	private static final String ERROR_JSON = "{\"code\":401,\"url\":\"%s\"}";

	/**
	 * return error message, instead of redirect to login page
	 */
	@Override
	protected boolean onAccessDenied(ServletRequest req, ServletResponse response) throws Exception {
		HttpServletRequest request = (HttpServletRequest) req;

		HttpServletResponse httpResponse = WebUtils.toHttp(response);
		httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

		response.setContentType("application/json");
		response.setCharacterEncoding("UTF-8");
		response.getWriter().printf(ERROR_JSON, request.getRequestURI());

		return false;
	}
}

ExternalCallFilter: 这里的校验 主要是 使用 排序后的请求参数,以及双方协定的固定的 clientDigest 进行 HMAC 的 md5 编码 进行校验

package com.peptalk.security;

import com.google.common.base.Joiner;
import com.peptalk.util.MapUtils;
import org.apache.shiro.web.filter.authc.UserFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.CollectionUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by xlch on 2016/7/1.
 */
public class ExternalCallFilter extends UserFilter {

    private static final String ERROR_JSON = "{\"code\":401,\"url\":\"%s\"}";
    private static final String PARAM_DIGEST = "digest";
    private static final String PARAM_USERNAME = "username";
    private static final String PARAM_TIMESTAMP = "timestamp";

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        String username = null;
        String clientDigest = null;
        String timestamp = null;
        if (request instanceof HttpServletRequest){
            HttpServletRequest httpServletRequest = (HttpServletRequest)request;
            username = httpServletRequest.getHeader(PARAM_USERNAME);
            clientDigest = httpServletRequest.getHeader(PARAM_DIGEST);
//            timestamp = httpServletRequest.getHeader(PARAM_TIMESTAMP);
            if (timestamp != null) {

            }
        }
        if (username == null || clientDigest == null) {
            this.loginFail(request,response);
        }
        Map<String, String[]> paramsMap = new HashMap<>(request.getParameterMap());
        paramsMap.remove(PARAM_DIGEST);
        paramsMap.remove(PARAM_USERNAME);
//        paramsMap.remove(PARAM_TIMESTAMP);
        String paramsStrSorted = "";
        if (!CollectionUtils.isEmpty(paramsMap)){
            Object [] paramsArray = MapUtils.mapValueToArray(paramsMap);
            String[] paramsArrSort = new String[paramsArray.length];
            for (int i = 0,length = paramsArray.length;i<length;i++){
                String [] str = (String [])paramsArray[i];
                paramsArrSort[i] = str[0];
            }
            Arrays.sort(paramsArrSort);
            paramsStrSorted = Joiner.on("").join(paramsArrSort);
        }
        ExternalToken token = new ExternalToken(username,paramsStrSorted,clientDigest);
        try {
            getSubject(request,response).login(token);
        } catch (Exception e) {
            this.loginFail(request,response);
            return false;
        }
        return true;
    }

    private void loginFail(ServletRequest req, ServletResponse response) throws IOException{
        HttpServletRequest request = (HttpServletRequest) req;

        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        response.getWriter().printf(ERROR_JSON, request.getRequestURI());
    }
}

MapUtils

    public static Object [] mapValueToArray(Map map){
        if (!CollectionUtils.isEmpty(map)){
            Object [] strArray =  map.values().toArray();
            return strArray;
        }
        return null;
    }

ExternalToken

package com.peptalk.security;

import org.apache.shiro.authc.AuthenticationToken;

import java.util.Map;

/**
 * Created by xlch on 2016/7/1.
 */
public class ExternalToken implements AuthenticationToken {

    private String username;
    private String msg;
    private String clientDigest;

    public ExternalToken(String username, String msg, String clientDigest) {
        this.username = username;
        this.msg = msg;
        this.clientDigest = clientDigest;
    }

    public String getClientDigest() {
        return clientDigest;
    }

    public void setClientDigest(String clientDigest) {
        this.clientDigest = clientDigest;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    @Override
    public Object getPrincipal() {
        return username;
    }

    @Override
    public Object getCredentials() {
        return clientDigest;
    }
}

通过 ExternalToken 的 login 方法调用 ExternalReam 进行认证

ExternalReam

package com.peptalk.security;

import com.peptalk.util.HMACUtil;
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.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created by xlch on 2016/6/30.
 */
public class ExternalReam extends AuthorizingRealm {
//设置哪个 token 会触发该验证
    private final static Logger logger = LoggerFactory.getLogger(ExternalReam.class);

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof ExternalToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.debug("{} {}() run ...","ExternalRealm","doGetAuthorizationInfo");
        SimpleAuthorizationInfo result = new SimpleAuthorizationInfo();
        result.addStringPermission("view");
        return result;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        ExternalToken externalToken = (ExternalToken)token;
        String username = externalToken.getUsername();
        String msg = externalToken.getMsg();
        String serverDigest = HMACUtil.hmacDigest(msg,HMACUtil.getKey(),HMACUtil.HMACMD5);
        return new SimpleAuthenticationInfo(username,serverDigest,getName());
    }
}

HMACUtil

package com.peptalk.util;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

/**
 * Created by xlch on 2016/7/1.
 */
public class HMACUtil {

    public static final String HMACSHA1 = "HmacSHA1";
    public static final String HMACMD5 = "HmacMD5";

    /**
     * 请求方和被请求方共同的密匙,务必保证一致
     * @return
     */
    public static String getKey(){
        return "important key";
    }

    /**
     *
     * @param msg          需要发送的消息
     * @param keyString   密钥
     * @param algorithm   算法类型 :HmacSHA1    HmacMD5   其实是散列函数不同
     * @return
     */
    public static String hmacDigest(String msg, String keyString, String algorithm) {
        String digest = null;
        try {
            SecretKeySpec key = new SecretKeySpec((keyString).getBytes("UTF-8"), algorithm);
            Mac mac = Mac.getInstance(algorithm);
            mac.init(key);

            byte[] bytes = mac.doFinal(msg.getBytes("ASCII"));

            StringBuffer hash = new StringBuffer();
            for (int i = 0; i < bytes.length; i++) {
                String hex = Integer.toHexString(0xFF & bytes[i]);
                if (hex.length() == 1) {
                    hash.append('0');
                }
                hash.append(hex);
            }
            digest = hash.toString();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return digest;
    }

    public static void main(String [] args){

    }
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值