欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

简单的Spring整合Shiro

程序员文章站 2022-06-14 08:45:34
...

shiro是流行的权限控制框架,这里参考了官网http://shiro.apache.org/reference.html和springside中的演示https://github.com/springside/springside4/wiki/Shiro-Security,并在此基础上实现自定义shiro输入参数。

最后是一些shiro中的定义,符合权限设计的国际惯例。这些定义可看可不看,有助于理解Shiro。

 

一. 环境

spring 3.2.6.RELEASE

shiro 1.2.2

ehcache 2.6.6

commons-codec 1.8

 

shiro和ehcache在maven应该引入的依赖如下

<!-- SECURITY begin -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${shiro.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!-- SECURITY end -->
<!-- CACHE begin -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>${ehcache.version}</version>
</dependency>
<!-- CACHE end -->

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>${commons-codec.version}</version>
</dependency>

 

 

二、加密方式

这里对选择的加密方式为,用户的password使用salt并迭代N次的sha-1式加密

salt是一组随机定长的byte数字,一般用byte数组装载

本文中用salt加密1024次,salt数组长度为8

用到了加密和解密,过程中用到了commons-codec,由于比较麻烦,所以借用springside中的utils,内容如下

 

Digests

import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import org.apache.commons.lang3.Validate;
import com.gqshao.common.exception.Exceptions;

public class Digests {

    private static final String SHA1 = "SHA-1";
    private static final String MD5 = "MD5";

    private static SecureRandom random = new SecureRandom();

    /**
     * 对输入字符串进行sha1散列.
     */
    public static byte[] sha1(byte[] input) {
        return digest(input, SHA1, null, 1);
    }

    public static byte[] sha1(byte[] input, byte[] salt) {
        return digest(input, SHA1, salt, 1);
    }

    public static byte[] sha1(byte[] input, byte[] salt, int iterations) {
        return digest(input, SHA1, salt, iterations);
    }

    /**
     * 对字符串进行散列, 支持md5与sha1算法.
     */
    private static byte[] digest(byte[] input, String algorithm, byte[] salt, int iterations) {
        try {
            MessageDigest digest = MessageDigest.getInstance(algorithm);

            if (salt != null) {
                digest.update(salt);
            }

            byte[] result = digest.digest(input);

            for (int i = 1; i < iterations; i++) {
                digest.reset();
                result = digest.digest(result);
            }
            return result;
        } catch (GeneralSecurityException e) {
            throw Exceptions.unchecked(e);
        }
    }

    /**
     * 生成随机的Byte[]作为salt.
     * 
     * @param numBytes byte数组的大小
     */
    public static byte[] generateSalt(int numBytes) {
        Validate.isTrue(numBytes > 0, "numBytes argument must be a positive integer (1 or larger)", numBytes);

        byte[] bytes = new byte[numBytes];
        random.nextBytes(bytes);
        return bytes;
    }

    /**
     * 对文件进行md5散列.
     */
    public static byte[] md5(InputStream input) throws IOException {
        return digest(input, MD5);
    }

    /**
     * 对文件进行sha1散列.
     */
    public static byte[] sha1(InputStream input) throws IOException {
        return digest(input, SHA1);
    }

    private static byte[] digest(InputStream input, String algorithm) throws IOException {
        try {
            MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
            int bufferLength = 8 * 1024;
            byte[] buffer = new byte[bufferLength];
            int read = input.read(buffer, 0, bufferLength);

            while (read > -1) {
                messageDigest.update(buffer, 0, read);
                read = input.read(buffer, 0, bufferLength);
            }

            return messageDigest.digest();
        } catch (GeneralSecurityException e) {
            throw Exceptions.unchecked(e);
        }
    }

}

 

 

 

Encodes

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringEscapeUtils;
import com.gqshao.common.exception.Exceptions;

public class Encodes {

    private static final String DEFAULT_URL_ENCODING = "UTF-8";
    private static final char[] BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
            .toCharArray();

    /**
     * Hex编码.
     */
    public static String encodeHex(byte[] input) {
        return Hex.encodeHexString(input);
    }

    /**
     * Hex解码.
     */
    public static byte[] decodeHex(String input) {
        try {
            return Hex.decodeHex(input.toCharArray());
        } catch (DecoderException e) {
            throw Exceptions.unchecked(e);
        }
    }

    /**
     * Base64编码.
     */
    public static String encodeBase64(byte[] input) {
        return Base64.encodeBase64String(input);
    }

    /**
     * Base64编码, URL安全(将Base64中的URL非法字符'+'和'/'转为'-'和'_', 见RFC3548).
     */
    public static String encodeUrlSafeBase64(byte[] input) {
        return Base64.encodeBase64URLSafeString(input);
    }

    /**
     * Base64解码.
     */
    public static byte[] decodeBase64(String input) {
        return Base64.decodeBase64(input);
    }

    /**
     * Base62编码。
     */
    public static String encodeBase62(byte[] input) {
        char[] chars = new char[input.length];
        for (int i = 0; i < input.length; i++) {
            chars[i] = BASE62[((input[i] & 0xFF) % BASE62.length)];
        }
        return new String(chars);
    }

    /**
     * Html 转码.
     */
    public static String escapeHtml(String html) {
        return StringEscapeUtils.escapeHtml4(html);
    }

    /**
     * Html 解码.
     */
    public static String unescapeHtml(String htmlEscaped) {
        return StringEscapeUtils.unescapeHtml4(htmlEscaped);
    }

    /**
     * Xml 转码.
     */
    public static String escapeXml(String xml) {
        return StringEscapeUtils.escapeXml(xml);
    }

    /**
     * Xml 解码.
     */
    public static String unescapeXml(String xmlEscaped) {
        return StringEscapeUtils.unescapeXml(xmlEscaped);
    }

    /**
     * URL 编码, Encode默认为UTF-8. 
     */
    public static String urlEncode(String part) {
        try {
            return URLEncoder.encode(part, DEFAULT_URL_ENCODING);
        } catch (UnsupportedEncodingException e) {
            throw Exceptions.unchecked(e);
        }
    }

    /**
     * URL 解码, Encode默认为UTF-8. 
     */
    public static String urlDecode(String part) {

        try {
            return URLDecoder.decode(part, DEFAULT_URL_ENCODING);
        } catch (UnsupportedEncodingException e) {
            throw Exceptions.unchecked(e);
        }
    }
}

 

 

 

 

用法如下:

// 得到8位盐
byte[] salts = Digests.generateSalt(SALT_SIZE);
// 将8位byte数组装换为spring
String salt = Encodes.encodeHex(salts);
// 将spring数组转化为8位byte数组
salts = Encodes.decodeHex(salt);

// 原密码
String password = "123456";
// 对密码加盐进行1024次SHA1加密
byte[] hashPassword = Digests.sha1(password.getBytes(), salts, 1024);
// 将加密后的密码数组转换成字符串
password = Encodes.encodeHex(hashPassword);

 

简单说一下就是用户输入密码,系统自动生成盐,并对密码加密。然后将盐的字符串跟加密后的密码字符串保存到数据库中。

 

 

三、配置

1.web.xml

主要内容如下,其他的如spring和MVC框架等配置自行添加

<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>development</param-value>
</context-param>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
    classpath*:/applicationContext.xml
    classpath*:/activiti/applicationContext-security.xml
</param-value>
</context-param>

<!-- Shiro Security filter -->
<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
</filter-mapping>

 

 

可以看到shiro是通过filter进行过滤的,所以如果还有其他filter注意放的位置,比如org.springframework.web.filter.CharacterEncodingFilter要放到前面,com.opensymphony.sitemesh.webapp.SiteMeshFilter这种就要放到它的后面。

 

2.applicationContext-shiro.xml和ehcache-shiro.xml

这两个配置文件是shiro的主要配置文件

applicationContext-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:util="http://www.springframework.org/schema/util"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
	http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd"
	default-lazy-init="true">
 

    <description>Shiro安全配置</description>

    <!-- Shiro's main business-tier object for web-enabled applications -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="shiroDbRealm" />
        <property name="cacheManager" ref="shiroEhcacheManager" />
    </bean>

    <!-- 項目自定义的Realm -->
    <bean id="shiroDbRealm" class="com.myapp.rbac.authentication.ShiroDbRealm" />

    <!-- Shiro Filter -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <!-- 用于调用Controller -->
        <property name="loginUrl" value="/login" />
        <property name="successUrl" value="/" />
        <!-- 自己实现的formAuthcFilter,加入key type-->
        <property name="filters">
            <util:map>
                <entry key="authc">
                    <bean class="com.myapp.rbac.authentication.CustomFormAuthenticationFilter"/>
                </entry>
           </util:map>
        </property>
         
        <property name="filterChainDefinitions">
            <value>
                /login = authc
                /logout = logout
                /static/** = anon
                /** = user
            </value>
        </property>
    </bean>

    <!-- 用户授权信息Cache, 采用EhCache -->
    <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:security/ehcache-shiro.xml" />
    </bean>

    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

    <!-- AOP式方法级权限检查 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
        <property name="proxyTargetClass" value="true" />
    </bean>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager" />
    </bean>
</beans>

 

注意:

1.com.myapp.rbac.authentication.ShiroDbRealm和com.myapp.rbac.authentication.CustomFormAuthenticationFilter是自定义的文件,一会儿有相关说明

2.shiroFilter属性中loginUrl会被ShiroFilter监控起来

3.shiroFilter属性中loginUrl会被successUrl是成功后返回的路径,这里面设置为“ / ”的原因是用的spring mvc

 

ehcache-shiro.xml

<ehcache updateCheck="false" name="shiroCache">
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            />
</ehcache>

 

 

四.实现类

为了演示自定义,所以从login.jsp(自己实现有个POST方法提交的表单即可)开始都有一个叫"custom"的无意义字段,实际项目中可以按需求选择方案,不一定自定义,直接用UsernamePasswordToken也很好。

1.authcToken及其实现类

用于将前端页面传入的参数封装成Token,传给登陆认证的方法。常用的有org.apache.shiro.authc.UsernamePasswordToken

这里自己实现了一个类

package com.myapp.rbac.authentication.token;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.HostAuthenticationToken;
import org.apache.shiro.authc.RememberMeAuthenticationToken;

public class CustomToken implements HostAuthenticationToken, RememberMeAuthenticationToken {

    private String loginName;
    private String password;
    private String host;
    private boolean rememberMe = false;
    private String custom;

    public CustomToken() {
    }

    public CustomToken(String loginName, String password) {
        this(loginName, password, false, null, null);
    }

    public CustomToken(String loginName, String password, String host) {
        this(loginName, password, false, host, null);
    }

    public CustomToken(String loginName, String password, boolean rememberMe) {
        this(loginName, password, rememberMe, null, null);
    }

    public CustomToken(String loginName, String password, boolean rememberMe, String host, String custom) {
        this.loginName = loginName;
        this.password = password;
        this.rememberMe = rememberMe;
        this.host = host;
        this.custom = custom;
    }

    public Object getPrincipal() {
        return getLoginName();
    }

    public Object getCredentials() {
        return getPassword();
    }

    public String getHost() {
        return host;
    }

    public boolean isRememberMe() {
        return rememberMe;
    }

    public void clear() {
        this.loginName = null;
        this.host = null;
        this.password = null;
        this.rememberMe = false;
        this.custom = null;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getName());
        sb.append(" - ");
        sb.append(loginName);
        sb.append(", rememberMe=").append(rememberMe);
        if (StringUtils.isNotBlank(host)) {
            sb.append(" (").append(host).append(")");
        }
        return sb.toString();
    }

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getCustom() {
        return custom;
    }

    public void setCustom(String custom) {
        this.custom = custom;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public void setRememberMe(boolean rememberMe) {
        this.rememberMe = rememberMe;
    }
}

  

 

 

2.User/Subject 

将一些期望保存的属性属性,封装成一个自定义POJO,会被放到session中

 

package com.myapp.rbac.authentication.domain;

import java.util.List;

import com.google.common.base.Objects;
import com.gqshao.common.util.Identities;

public class ShiroUser implements java.io.Serializable {

    private static final long serialVersionUID = -2649983064333269618L;

    private String id;
    private String loginName;
    private List<String> roles;
    private List<String> permissions;
    private String ip;
    // 记录额外的一些信息
    private String custom;

    public ShiroUser() {
    }

    public ShiroUser(User user) {
        this.id = Identities.uuid();
        this.loginName = user.getLoginName();
    }

    public ShiroUser(String id, String loginName, String ip, String custom) {
        this.id = id;
        this.loginName = loginName;
        this.ip = ip;
        this.custom = custom;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public List<String> getRoles() {
        return roles;
    }

    public void setRoles(List<String> roles) {
        this.roles = roles;
    }

    public List<String> getPermissions() {
        return permissions;
    }

    public void setPermissions(List<String> permissions) {
        this.permissions = permissions;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public String getLoginName() {
        return loginName;
    }

    public void setLoginName(String loginName) {
        this.loginName = loginName;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id, loginName, custom);
    }

    public String toString() {
        return id + ":" + loginName + "- " + custom;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        } else if (this.hashCode() == obj.hashCode()) {
            return true;
        } else {
            return false;
        }
    }

}

 

 

 

3.AuthenticatingFilter及其实现类

AuthenticatingFilter主要与将前端提交的表单信息封装成Token,与authcToken的实现类要对应起来。比如与org.apache.shiro.authc.UsernamePasswordToken对应的是org.apache.shiro.web.filter.authc.FormAuthenticationFilter。

这里需要自定义一个CustomFormAuthenticationFilter,用于配合CustomToken。

 

package com.myapp.rbac.authentication.filter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomFormAuthenticationFilter extends AuthenticatingFilter {

    private static final Logger log = LoggerFactory.getLogger(CustomFormAuthenticationFilter.class);

    public static final String DEFAULT_ERROR_KEY_ATTRIBUTE_NAME = "shiroLoginFailure";

    public static final String DEFAULT_LOGINNAME_PARAM = "loginName";
    public static final String DEFAULT_PASSWORD_PARAM = "password";
    public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";
    // 自定义的输入字段
    public static final String DEFAULT_CUSTOM_PARAM = "custom";

    private String loginNameParam = DEFAULT_LOGINNAME_PARAM;
    private String passwordParam = DEFAULT_PASSWORD_PARAM;
    private String rememberMeParam = DEFAULT_REMEMBER_ME_PARAM;
    private String customParam = DEFAULT_CUSTOM_PARAM;

    private String failureKeyAttribute = DEFAULT_ERROR_KEY_ATTRIBUTE_NAME;

    public CustomFormAuthenticationFilter() {
        setLoginUrl(DEFAULT_LOGIN_URL);
    }

    @Override
    public void setLoginUrl(String loginUrl) {
        String previous = getLoginUrl();
        if (previous != null) {
            this.appliedPaths.remove(previous);
        }
        super.setLoginUrl(loginUrl);
        if (log.isTraceEnabled()) {
            log.trace("Adding login url to applied paths.");
        }
        this.appliedPaths.put(getLoginUrl(), null);
    }

    /**
     * 在访问被拒绝
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                return executeLogin(request, response);
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("Login page view.");
                }
                return true;
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Attempting to access a path which requires authentication.  Forwarding to the "
                        + "Authentication url [" + getLoginUrl() + "]");
            }

            saveRequestAndRedirectToLogin(request, response);
            return false;
        }
    }

    /**
     * 创建自定义的令牌
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String loginName = getLoginName(request);
        String password = getPassword(request);
        boolean rememberMe = isRememberMe(request);
        String host = getHost(request);
        String custom = getCustom(request);
        return new CustomToken(loginName, password, rememberMe, host, custom);
    }

    protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
        return (request instanceof HttpServletRequest)
                && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
    }

    protected boolean isRememberMe(ServletRequest request) {
        return WebUtils.isTrue(request, getRememberMeParam());
    }

    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
            ServletResponse response) throws Exception {
        issueSuccessRedirect(request, response);
        return false;
    }

    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
            ServletRequest request, ServletResponse response) {
        setFailureAttribute(request, e);
        return true;
    }

    protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
        String className = ae.getClass().getName();
        request.setAttribute(getFailureKeyAttribute(), className);
    }

    protected String getLoginName(ServletRequest request) {
        return WebUtils.getCleanParam(request, getLoginNameParam());
    }

    protected String getPassword(ServletRequest request) {
        return WebUtils.getCleanParam(request, getPasswordParam());
    }

    protected String getCustom(ServletRequest request) {
        return WebUtils.getCleanParam(request, getCustomParam());
    }

    public String getLoginNameParam() {
        return loginNameParam;
    }

    public void setLoginNameParam(String loginNameParam) {
        this.loginNameParam = loginNameParam;
    }

    public String getPasswordParam() {
        return passwordParam;
    }

    public void setPasswordParam(String passwordParam) {
        this.passwordParam = passwordParam;
    }

    public String getRememberMeParam() {
        return rememberMeParam;
    }

    public void setRememberMeParam(String rememberMeParam) {
        this.rememberMeParam = rememberMeParam;
    }

    public String getCustomParam() {
        return customParam;
    }

    public void setCustomParam(String customParam) {
        this.customParam = customParam;
    }

    public String getFailureKeyAttribute() {
        return failureKeyAttribute;
    }

    public void setFailureKeyAttribute(String failureKeyAttribute) {
        this.failureKeyAttribute = failureKeyAttribute;
    }

}

 

 

 

4.Realm实现

Realm用于登陆认证和权限的鉴权工作

 

package com.myapp.rbac.authentication.realm;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.PostConstruct;

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.HashedCredentialsMatcher;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import com.myapp.rbac.authentication..domain.ShiroUser;
import com.genertech.commons.core.utils.Encodes;
import com.myapp.rbac.authentication.token.CustomToken;
import com.myapp.rbac.domain.Permission;
import com.myapp.rbac.domain.Role;
import com.myapp.rbac.domain.User;
import com.myapp.rbac.service.PermissionService;
import com.myapp.rbac.service.RoleService;
import com.myapp.rbac.service.UserService;

public class ShiroDbRealm extends AuthorizingRealm {

    public static final String HASH_ALGORITHM = "SHA-1";
    public static final int SALT_SIZE = 8;
    public static final int HASH_INTERATIONS = 1024;

    @Autowired
    private UserService userService;

    @Autowired
	protected PermissionService permissionService;
	@Autowired
	protected RoleService roleService;
    
    
    public ShiroDbRealm() {
        super();
        setAuthenticationTokenClass(CustomToken.class);
    }

    /**
     * 认证回调函数,登录时调用.
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken){
        CustomToken token = (CustomToken) authcToken;
        String loginName = token.getLoginName();
        String host = token.getHost();
        User user = userService.getUserByLoginName(loginName);
        if (user != null) {
            ShiroUser root = new ShiroUser(user.getId(), loginName, user.getName(),
                    user.getIsAdmin() == 1 ? true : false, host);
            byte[] salt = Encodes.decodeHex(user.getSalt());
            return new SimpleAuthenticationInfo(root, user.getPassword(), ByteSource.Util.bytes(salt),
                    getName());
        } else {
            return null;
        }

    }

    /**
     * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用.
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    	ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

		List<Role> roleList = null;// 角色集合//
		List<String> pList = new ArrayList<String>();
		List<Permission> listP = null;// 权限集合//

		if (shiroUser.isAdmin()) {
			// 超级管理员获取所有角色//
			roleList = roleService.getAll();
			// 超级管理员获取所有权限//
			listP = permissionService.getAllPermissions();
		} else {
			roleList = roleService.getRolesByUserId(shiroUser.getId());
			listP = permissionService.getByUserId(shiroUser.getId());
		}
		// 遍历角色//
		for (Role role : roleList) {
			info.addRole(role.getName());
		}
		// 遍历权限//
		for (Permission per : listP) {
			if (per != null) {
				String[] ps = per.getCode().split(";");
				for (int i = 0; i < ps.length; i++) {
					pList.add(ps[i]);
				}
			}
		}
		if (shiroUser.isAdmin()) {
			pList.add("permissionmgt:administrator");
		}
		// 基于Permission的权限信息//
		info.addStringPermissions(pList);
		return info;
    }

    /**
     * 设定Password校验的Hash算法与迭代次数.
     */
    @PostConstruct
    public void initCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(HASH_ALGORITHM);
        matcher.setHashIterations(HASH_INTERATIONS);
        setCredentialsMatcher(matcher);
    }

}

 

注意

1.实现登陆的前提是通过POST方法,提交到 loginUrl 配置的路径上。

2.doGetAuthenticationInfo完成登陆的认证工作。Shiro通过return的SimpleAuthenticationInfo中的参数,自动判断password是否正确。再次提醒,user保存在数据库中的password是通过salt加密过的密码;

3.认证的方法通过initCredentialsMatcher设置;

4.doGetAuthorizationInfo方法完成的是鉴权工作,这里需要注意的事情是,由于权限和角色都是通过整合Ehcache,缓存起来,所以当缓存失效的时候,会再次鉴权。

5.注意权限定义中 : 号有特殊用途,不要随便使用。

 

5.控制器

最后完成一个控制器,用于登陆跳转到登陆页面,或登陆失败后的页面调整

这里用的是spring mvc

 

@Controller
public class LoginController {
    Logger logger = LoggerFactory.getLogger(LoginController.class);

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login(Model model) {
        return "common/login";
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String fail(@RequestParam(CustomFormAuthenticationFilter.DEFAULT_LOGINNAME_PARAM) String userName,
            Model model) {
        model.addAttribute(CustomFormAuthenticationFilter.DEFAULT_LOGINNAME_PARAM, userName);
        return "common/login";
    }

}

 

 

五.使用

JSP内容控制

页面部分可以通过导入  <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> 使用标签

常用的有hasPermission、hasAnyRole等,参考http://shiro.apache.org/web.html#Web-taglibrary

 

方法级控制

@RequiresPermissions("User:Edit")

@RequiresRoles(value = { "Admin", "User" }, logical = Logical.OR)等等

可以参考http://shiro.apache.org/authorization.html#Authorization-ProgrammaticAuthorization

 

通过Shiro保存到Session中的对象可以通过以下方法得到

Subject shiroUser = SecurityUtils.getSubject();

 

 

六. 概念

· Authentication
身份验证是验证Subject 身份的过程——实质上是证明某些人是否真的是他们所说的他们是谁。当认证尝试成
功后,应用程序能够相信该subject 被保证是其所期望的。


· Authorization
授权,又称为访问控制,是决定一个user/Subject 是否被允许做某事的过程。它通常是通过检查和解释。 

 

· Subject
的角色和权限(见下文),然后允许或拒绝到一个请求的资源或功能来完成的。


· Cipher
密码是进行加密或解密的一种算法。该算法一般依赖于一块被称为key 的信息。基于不同的key 的加密算法
也是不一样的,所有解密没有它是非常困难的。
密码有不同的表现形式。分组密码致力于符号块,通常是固定大小的,而流密码致力于连续的符号流。对称
性密码加密和解密使用相同的密钥(key),而非对称性加密使用不同的密钥。如果非对称性加密的密钥不能
从其他地方得到,那么可以创建公钥/私钥对公开共享。


· Credential
凭证是一块信息,用来验证user/Subject 的身份。在认证尝试期间,一个(或多个)凭证与Principals(s)被一
同提交,来验证user/Subject 所提交的确实是所关联的用户。证书通常是非常秘密的东西,只有特定的
user/Subject 才知道,如密码或PGP 密钥或生物属性或类似的机制。
这个想法是为principal 设置的,只有一个人会知道正确的证书来“匹配”该principal。如果当前user/Subject
提供了正确的凭证匹配了存储在系统中的,那么系统可以假定并信任当前user/Subject 是真的他们所说的他们
是谁。信任度随着更安全的凭证类型加深(如,生物识别签名 > 密码)。


· Cryptography
加密是保护信息不受不希望的访问的习惯做法,通过隐藏信息或将它转化成无意义的东西,这样没人可以理
解它。Shiro 致力于加密的两个核心要素:加密数据的密码,如使用公钥或私钥的邮件,以及散列表(也称消
息摘要),它对数据进行不可逆的加密,如密码。


· Hash
散列函数是单向的,不可逆转的输入源,有时也被称为消息,在一个编码的哈希值内部,有时也被称为消息
摘要。它通常用于密码,数字指纹,或以字节数组为基础的数据。


· Permission
权限,至少按照Shiro 的解释,是在应用程序中描述原始功能的一份声明并没有更多的功能。权限是在安全策
略中最低级别的概念。它们仅定义了应用程序能够做“什么”。它们没有说明“谁”能够执行这些操作。权
限只是行为的声明,仅此而已。
一些权限的例子:
· 打开文件
· 浏览'/user/list'页面
· 打印文档
· 删除'jsmith'用户


· Principal
Principal 是一个应用程序用户(Subject)的任何标志属性。“标志属性”可以是任何对你应用程序有意义的东
西——用户名,姓,名,社会安全号码,用户ID 等。这就是它——没什么古怪的。
Shiro 也引用一些我们称之为Subject 的primary principal 的东西。一个primary principal 是在整个应用程序中唯
一标识Subject 的principal。理想的primary principal 是用户名或RDBMS 用户表主键——用户ID。对于在应用
程序中的用户(Subject)来说,只有一个primary principal


· Realm
Realm 是一个能够访问应用程序特定的安全数据(如用户,角色和权限)的组件。它可以被看作是一个特定
安全的DAO(Data Access Object)。Realm 将这些应用程序特定的数据转换成Shiro 能够理解的格式,这样Shiro
反过来能够提供一个单一的易于理解的Subject 编程API,无论有多少数据源存在或无论你的数据是什么样的
应用程序特定的格式。
Realm 通常和数据源是一对一的对应关系,如关系数据库,LDAP 目录,文件系统,或其他类似资源。因此,
Realm 接口的实现使用数据源特定的API 来展示授权数据(角色,权限等),如JDBC,文件IO,Hibernate 或
JPA,或其他数据访问API。


· Role
基于你对话的对象,一个角色的定义是可以多变的。在许多应用程序中,它充其量是个模糊不清的概念,人
们用它来隐式定义安全策略。Shiro 偏向于把角色简单地解释为一组命名的权限的集合。这就是它——一个应
用程序的唯一名称,聚集一个或多个权限声明。
这是一个比许多应用程序使用的隐式的定义更为具体的定义。如果你选择了你的数据模型反映Shiro 的假设,
你会发现将有更多控制安全策略的权力。


· Session
会话是一个在一段时间内有状态的数据,其上下文与一个单一的与软件系统交互的user/Subject 相关联。当
Subject 使用应用程序时,能够从会话中添加/读取/删除数据,并且应用程序稍后能够在需要的地方使用该数
据。会话会被终止,由于user/Subject 注销或会话不活动而超时。
对于那些熟悉HttpSession 的,Shiro Session 服务于同一目标,除了Shiro 会话能够在任何环境下使用,甚至在
没有Servlet 容器或EJB 容器的环境。


· Subject
Subject 只是一个精挑细选的安全术语,基本上的意思是一个应用程序用户的安全特定的“视图”。然而Subject
不总是需要反映为一个人——它可以代表一个调用你应用程序的外部进程,或许是一个系统帐户的守护进程,
在一段时间内执行一些间歇性的东西(如一个cron job)。它基本上是任何使用应用程序做某事的实体的一个
代表。