SSM整合shiro

2.3.SSM整合

Shiro 的组件都是 JavaBean/POJO 式的组件,所以非常容易使用 Spring 进行组件管理,可以非常方便的从 ini 配置迁移到 Spring 进行管理,且支持 JavaSE 应用及 Web 应用的集成。

2.3.1.pom文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0modelVersion>
    <groupId>net.wanhogroupId>
    <artifactId>shiroTeachingartifactId>
    <packaging>warpackaging>
    <version>1.0-SNAPSHOTversion>
    <name>shiroTeaching Maven Webappname>
    <url>http://maven.apache.orgurl>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <spring.version>4.2.4.RELEASEspring.version>
        <mybatis.version>3.0.6mybatis.version>
        <jackson.version>1.9.10jackson.version>
        <shiro.version>1.2.2shiro.version>
        <mybatis.spring.version>1.2.2mybatis.spring.version>
        <jstl.version>1.2jstl.version>
        <servlet-api.version>2.5servlet-api.version>
        <jsp-api.version>2.0jsp-api.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-log4j12artifactId>
            <version>1.7.2version>
        dependency>
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-webartifactId>
            <version>${shiro.version}version>
        dependency>
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-ehcacheartifactId>
            <version>${shiro.version}version>
        dependency>
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-springartifactId>
            <version>${shiro.version}version>
        dependency>
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-quartzartifactId>
            <version>${shiro.version}version>
        dependency>
        <dependency>
            <groupId>commons-logginggroupId>
            <artifactId>commons-loggingartifactId>
            <version>1.1.3version>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.18version>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.0.11version>
        dependency>
        
        <dependency>
            <groupId>org.mybatisgroupId>
            <artifactId>mybatisartifactId>
            <version>${mybatis.version}version>
        dependency>
        <dependency>
            <groupId>org.mybatisgroupId>
            <artifactId>mybatis-springartifactId>
            <version>${mybatis.spring.version}version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-beansartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-webmvcartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-jdbcartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-aspectsartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-jmsartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-context-supportartifactId>
            <version>${spring.version}version>
        dependency>
        
        <dependency>
            <groupId>jstlgroupId>
            <artifactId>jstlartifactId>
            <version>${jstl.version}version>
        dependency>
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>servlet-apiartifactId>
            <version>${servlet-api.version}version>
            <scope>providedscope>
        dependency>
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>jsp-apiartifactId>
            <version>${jsp-api.version}version>
            <scope>providedscope>
        dependency>
        <dependency>
            <groupId>org.codehaus.jacksongroupId>
            <artifactId>jackson-mapper-aslartifactId>
            <version>${jackson.version}version>
        dependency>

    dependencies>
    <build>
        <finalName>shiroTeachingfinalName>

        <plugins>
            
            <plugin>
                <groupId>org.apache.tomcat.mavengroupId>
                <artifactId>tomcat7-maven-pluginartifactId>
                <configuration>
                    <path>/path>
                    <port>8080port>
                configuration>
            plugin>
        plugins>
    build>
project>

2.3.2.spring配置文件

applicationContext-shiro


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    


    

    
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:shiro/ehcache.xml"/>
    bean>
    
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashAlgorithmName" value="md5" />
        <property name="hashIterations" value="2" />
    bean>
    
    <bean id="userRealm" class="net.wanho.shiro.UserRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher" />
        <property name="cachingEnabled" value="false" />
    bean>
    
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="userRealm" />
        <property name="cacheManager" ref="cacheManager"/>
        <property name="rememberMeManager" ref="rememberMeManager"/>
    bean>
    
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    bean>

    
    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" />
        <property name="arguments" ref="securityManager" />
    bean>
    
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <property name="cookie" ref="rememberMeCookie" />
    bean>

    
    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg value="rememberMe" />
        
        <property name="maxAge" value="604800" />
    bean>
    
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login/toLogin" />
        <property name="unauthorizedUrl" value="/error.html"/>
        <property name="filterChainDefinitions">
            <value>
                
                /login/toLogin = anon
                /login/checkLogin = anon
                /login/logout = authc
                /** = user
            value>
        property>
    bean>
    
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
                <prop key="org.apache.shiro.authz.UnauthorizedException">/errorprop>
            props>
        property>
    bean>


    
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />


beans>
  1. shiroFilter:此处使用 ShiroFilterFactoryBean 来创建 ShiroFilter 过滤器;filters 属性用于定义自己的过滤器,即 ini 配置中的 [filters] 部分;filterChainDefinitions 用于声明 url 和 filter 的关系,即 ini 配置中的 [urls] 部分。

    applicationContext-dao


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
   http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    
    <context:property-placeholder location="classpath:resource/*.properties"/>
    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="maxActive" value="10">property    >
        <property name="minIdle" value="5">property>
    bean>
    
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation"  value="classpath:mybatis/SqlMapConfig.xml"/>
        <property name="dataSource" ref="dataSource">property>
    bean>

    
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="net.wanho.mapper">property>
    bean>



beans>

applicationContext-trans


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
   http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

    
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        
        <property name="dataSource" ref="dataSource">property>
    bean>

    
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            
            <tx:method name="save*" propagation="REQUIRED"/>
            <tx:method name="insert*" propagation="REQUIRED"/>
            <tx:method name="add*" propagation="REQUIRED"/>
            <tx:method name="create*" propagation="REQUIRED"/>
            <tx:method name="delete*" propagation="REQUIRED"/>
            <tx:method name="update*" propagation="REQUIRED"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
            <tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
        tx:attributes>
    tx:advice>
    <aop:config>
        <aop:advisor advice-ref="txAdvice" pointcut="execution(* net.wanho.service.*.*(..))"/>
    aop:config>
beans>

2.3.3.springMVC配置文件

springMVC.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    
    <context:property-placeholder location="classpath:resource/*.properties"/>
    
    <mvc:annotation-driven/>

    <context:component-scan base-package="net.wanho.controller"/>

    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp">property>
    bean>
	
    <aop:config proxy-target-class="true">aop:config>
beans>

web.xml

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         version="2.5"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

  <display-name>smePlantformdisplay-name>
  <welcome-file-list>
    <welcome-file>index.htmlwelcome-file>
    <welcome-file>index.htmwelcome-file>
    <welcome-file>index.jspwelcome-file>
    <welcome-file>default.htmlwelcome-file>
    <welcome-file>default.htmwelcome-file>
    <welcome-file>default.jspwelcome-file>
  welcome-file-list>
  <context-param>
    <param-name>contextConfigLocationparam-name>
    <param-value>classpath:spring/applicationContext-*.xmlparam-value>
  context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
  listener>

  
  <filter>
    <filter-name>characterEncodingFilterfilter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
    <init-param>
      <param-name>encodingparam-name>
      <param-value>utf-8param-value>
    init-param>
  filter>
  <filter-mapping>
    <filter-name>characterEncodingFilterfilter-name>
    <url-pattern>/*url-pattern>
  filter-mapping>

  
  <servlet>
    <servlet-name>springServletservlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
    <init-param>
      <param-name>contextConfigLocationparam-name>
      <param-value>
        classpath:spring/springmvc.xml
      param-value>
    init-param>
    <load-on-startup>1load-on-startup>
  servlet>
  <servlet-mapping>
    <servlet-name>springServletservlet-name>
    <url-pattern>/url-pattern>
  servlet-mapping>

  <filter>
    <filter-name>shiroFilterfilter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
    <init-param>
      <param-name>targetFilterLifecycleparam-name>
      <param-value>trueparam-value>
    init-param>
  filter>
  <filter-mapping>
    <filter-name>shiroFilterfilter-name>
    <url-pattern>/*url-pattern>
  filter-mapping>
web-app>

2.3.4 实现shiro整合ehcache


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

    
    <cache name="authorizationCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="1"
           overflowToDisk="false"
           statistics="true">
    cache>
    
    <cache name="authenticationCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="1"
           overflowToDisk="false"
           statistics="true">
    cache>
    
    <cache name="shiro-activeSessionCache"
           maxEntriesLocalHeap="10000"
           overflowToDisk="false"
           eternal="false"
           diskPersistent="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="1"
           statistics="true">
    cache>
ehcache>

2.4.密码比对,MD5&盐值加密

2.4.1.散列算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个 salt(盐),比如加密密码 “admin”,产生的散列值是 “21232f297a57a5a743894a0e4a801fc3”,可以到一些 md5 解密网站很容易的通过散列值得到密码 “admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐);这样散列的对象是 “密码 + 用户名 +ID”,这样生成的散列值相对来说更难破解。

String str = "hello";
String salt = "123";
String md5 = new Md5Hash(str, salt).toString();//还可以转换为 toBase64()/toHex() 

Shiro 还提供了通用的散列支持:

String str = "hello";
String salt = "123";
//内部使用MessageDigest
String simpleHash = new SimpleHash("MD5", str, salt).toString()

通过调用 SimpleHash 时指定散列算法,其内部使用了 Java 的 MessageDigest 实现。

2.4.2.HashedCredentialsMatcher 实现密码验证服务

Shiro 提供了 CredentialsMatcher 的散列实现 HashedCredentialsMatcher,和之前的 PasswordMatcher 不同的是,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且生成密码散列值的算法需要自己写,因为能提供自己的盐。

1、生成密码散列值

此处我们使用 MD5 算法,“密码 + 盐(用户名 + 随机数)” 的方式生成散列值:

String algorithmName = "md5";
String username = "liu";
String password = "123";
String salt1 = username;
String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex();
int hashIterations = 2;
SimpleHash hash = new SimpleHash(algorithmName, password, salt1 + salt2, hashIterations);
String encodedPassword = hash.toHex()

如果要写用户模块,需要在新增用户 / 重置密码时使用如上算法保存密码,将生成的密码及 salt2 存入数据库(因为我们的散列算法是:md5(md5(密码 +username+salt2)))。

2、功能实现

User

public class TUser {
    private Integer tUserId;

    private String tUserName;

    private String tUserAccount;

    private String tUserPassword;

    private String tSort;

    private String tUserType;

    private Integer tParentUserId;

    private Integer departmentId;

    private String tStatus;
}
public class UserService implements IUserService{
    /**
     * 获取所有的角色
     * @param username
     * @return
     */
    @Override
    public Set<String> findRoles(String username) {
        Set<String> set = new HashSet<>();
        set.add("admin");
        return set;
    }

    /**
     * 获取所有的权限
     * @param username
     * @return
     */
    @Override
    public Set<String> findPermissions(String username) {
        Set<String> set = new HashSet<>();
        set.add("dustin:test");
        return set;
    }

    /**
     * 根据用户名获取用户信息
     * @param username
     * @return
     */
    @Override
    public TUser findByUsername(String username) {
        TUser user = new TUser();
        user.settUserName(username);
        user.settUserPassword(shiroMD5("123456"));
        user.settSort("abcd");
        return user;
    }

    //shiro的密码加密,参数hashIterations表示加密次数,应该跟配置文件中的相同
    private String shiroMD5(String credentials){
        //加密方式
        String hashAlgorithmName = "MD5";
        //String credentials = "123456";
        //盐
        ByteSource credentialsSalt = ByteSource.Util.bytes("dustin"+"abcd");
        //加密次数
        int hashIterations = 2;
        Object obj = new SimpleHash(hashAlgorithmName, credentials, credentialsSalt, hashIterations);
        return obj.toString();
    }
}
public class UserRealm extends AuthorizingRealm {

    @Resource
    private IUserService userService;

    /**
     * 获取角色与权限
     *doGetAuthorizationInfo执行时机有三个,如下:
     *  1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
     *  2、@RequiresRoles("admin") :在方法上加注解的时候;
     *  3、@shiro.hasPermission name = "admin"/@shiro.hasPermission:"dustin:test"在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String)principals.getPrimaryPrincipal();

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //获取用户所有角色
        authorizationInfo.setRoles(userService.findRoles(username));
        //获取用户所有权限
        authorizationInfo.setStringPermissions(userService.findPermissions(username));

        return authorizationInfo;
    }

    /**
     * 登录信息验证
     *
     * 1.doGetAuthenticationInfo执行时机如下
     * 当调用Subject currentUser = SecurityUtils.getSubject();
     * currentUser.login(token);
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        String username = (String)token.getPrincipal();

        TUser user = userService.findByUsername(username);

        if(user == null) {
            throw new UnknownAccountException();//没找到帐号
        }

        if(Boolean.TRUE.equals(user.gettStatus())) {
            throw new LockedAccountException(); //帐号锁定
        }

        //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user.gettUserName(), //用户名
                user.gettUserPassword(), //密码
                ByteSource.Util.bytes(user.gettUserName()+user.gettSort()),//salt=username+salt
                getName()  //realm name
        );
        return authenticationInfo;
    }


    @Override
    public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }

    @Override
    public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }

    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

    public void clearAllCachedAuthorizationInfo() {
        getAuthorizationCache().clear();
    }

    public void clearAllCachedAuthenticationInfo() {
        getAuthenticationCache().clear();
    }

    public void clearAllCache() {
        clearAllCachedAuthenticationInfo();
        clearAllCachedAuthorizationInfo();

    }
}

public class loginController {

    @RequestMapping("/toLogin")
    public String tologin(HttpServletRequest request, HttpServletResponse response, Model model){
        System.out.println("来自IP[" + request.getRemoteHost() + "]的访问");
        return "login";
    }

    /**
     * 验证用户名和密码
     * @return
     */
    @RequestMapping("/checkLogin")
    public String login(String username,String password,ModelMap map) {

        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        Subject currentUser = SecurityUtils.getSubject();
        try {
            //使用shiro来验证
            if (!currentUser.isAuthenticated()){
                //这里是记住我,记cookie
                token.setRememberMe(false);
                currentUser.login(token);//验证角色和权限
            }


        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            map.put("code","登录失败:"+"账号密码不正确");
        }
        return JSONUtils.toJSONString(map);
    }


    @RequestMapping("/isLogin")
    @ResponseBody
    public String isLogin() {
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.isAuthenticated();
        return String.valueOf(currentUser.isAuthenticated());
    }


    @RequestMapping("/logout")
    @ResponseBody
    public String logout() {
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.logout();
        return "logout success";
    }
}
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>


    
    登录页面
    <%@include file="/taglib.jsp" %>


username:
password:

此处就是把步骤 1 中生成的相应数据组装为 SimpleAuthenticationInfo,通过 SimpleAuthenticationInfo 的 credentialsSalt 设置盐,HashedCredentialsMatcher 会自动识别这个盐。

  • saltStyle 表示使用密码 + 盐的机制,authenticationQuery 第一列是密码,第二列是盐;
  • 通过 authenticationQuery 指定密码及盐查询 SQL;

此处还要注意 Shiro 默认使用了 apache commons BeanUtils,默认是不进行 Enum 类型转型的,此时需要自己注册一个 Enum 转换器 “BeanUtilsBean.getInstance().getConvertUtils().register(new EnumConverter(), JdbcRealm.SaltStyle.class);” 具体请参考示例 “com.github.zhangkaitao.shiro.chapter5.hash.PasswordTest” 中的代码。

3、spring配置


<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
    <property name="hashAlgorithmName" value="md5" />
    <property name="hashIterations" value="2" />
bean>

此处最需要注意的就是 HashedCredentialsMatcher 的算法需要和生成密码时的算法一样。另外 HashedCredentialsMatcher 会自动根据 AuthenticationInfo 的类型是否是 SaltedAuthenticationInfo 来获取 credentialsSalt 盐。

2.4.3.密码重试次数限制

如在 1 个小时内密码最多重试 5 次,如果尝试次数超过 5 次就锁定 1 小时,1 小时后可再次重试,如果还是重试失败,可以锁定如 1 天,以此类推,防止密码被暴力破解。我们通过继承 HashedCredentialsMatcher,且使用 Ehcache 记录重试次数和超时时间。

public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
       String username = (String)token.getPrincipal();
        //retry count + 1
        Element element = passwordRetryCache.get(username);
        if(element == null) {
            element = new Element(username , new AtomicInteger(0));
            passwordRetryCache.put(element);
        }
        AtomicInteger retryCount = (AtomicInteger)element.getObjectValue();
        if(retryCount.incrementAndGet() > 5) {
            //if retry count > 5 throw
            throw new ExcessiveAttemptsException();
        }
        boolean matches = super.doCredentialsMatch(token, info);
        if(matches) {
            //clear retry count
            passwordRetryCache.remove(username);
        }
        return matches;
}

如上代码逻辑比较简单,即如果密码输入正确清除 cache 中的记录;否则 cache 中的重试次数 +1,如果超出 5 次那么抛出异常表示超出重试次数了。

2.5.标签&权限注解

2.5.1.jsp标签

Shiro 提供了 JSTL 标签用于在 JSP/GSP 页面进行权限控制,如根据登录用户显示相应的页面按钮

导入标签库

<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

标签库定义在 shiro-web.jar 包下的 META-INF/shiro.tld 中定义。

2.5.1.1.guest 标签


欢迎游客访问,登录
		

用户没有身份验证时显示相应信息,即游客访问信息。

2.5.1.2.user 标签


        已认证通过的用户,setRememberMe = true
		

用户已经身份验证 / 记住我登录后显示相应的信息。

2.5.1.3.authenticated 标签


    用户[]已身份验证通过

用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的。

2.5.1.4.notAuthenticated 标签


    未身份验证(包括记住我)
 	

用户已经身份验证通过,即没有调用 Subject.login 进行登录,包括记住我自动登录的也属于未进行身份验证。

2.5.1.5.principal 标签

显示用户身份信息,默认调用 Subject.getPrincipal() 获取,即 Primary Principal。

相当于 Subject.getPrincipals().oneByType(String.class)。

相当于 ((User)Subject.getPrincipals()).getUsername()。

2.5.1.6.hasRole 标签


    用户[]拥有角色admin

如果当前 Subject 有角色将显示 body 体内容。

2.5.1.7.hasAnyRoles 标签


    用户[]拥有角色admin或user
;

如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容。

2.5.1.8.hasPermission 标签


    用户[]拥有权限user:create
 

如果当前 Subject 有权限将显示 body 体内容。

2.5.1.8.lacksPermission 标签


    用户[]没有权限org:create
 

如果当前 Subject 没有权限将显示 body 体内容。

2.5.2.权限注解

Shiro 提供了相应的注解用于权限控制,如果使用这些注解就需要使用 AOP 的功能来进行判断,如 Spring AOP;Shiro 提供了 Spring AOP 集成用于权限注解的解析和验证。

@RequiresAuthentication

表示当前 Subject 已经通过 login 进行了身份验证;即 Subject.isAuthenticated() 返回 true。

@RequiresUser

表示当前 Subject 已经身份验证或者通过记住我登录的。

@RequiresGuest

表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。

@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)

表示当前 Subject 需要角色 admin 和 user。

@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)

表示当前 Subject 需要权限 user:a 或 user:b。

此处使用了 Spring MVC 来测试 Shiro 注解,当然 Shiro 注解不仅仅可以在 web 环境使用,在独立的 JavaSE 中也是可以用的,此处只是以 web 为例了。

在 spring-mvc.xml 配置文件添加 Shiro Spring AOP 权限注解的支持:

<aop:config proxy-target-class="true">aop:config>
<bean class="
org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
    <property name="securityManager" ref="securityManager"/>
bean>

如上配置用于开启 Shiro Spring AOP 权限注解的支持; 表示代理类。

接着就可以在相应的控制器(Controller)中使用如下方式进行注解:

@RequiresRoles("admin")
@RequestMapping("/hello2")
public String hello2() {
    return "success";
}

访问 hello2 方法的前提是当前用户有 admin 角色。

2.6.会话管理(了解)

Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储 / 持久化、容器无关的集群、失效 / 过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。即直接使用 Shiro 的会话管理可以直接替换如 Web 容器的会话管理。

2.6.1.会话

所谓会话,即用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。

Shiro 的会话支持不仅可以在普通的 JavaSE 应用中使用,也可以在 JavaEE 应用中使用,如 web 应用。且使用方式是一致的。

login("classpath:shiro.ini", "zhang", "123");
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession()

登录成功后使用 Subject.getSession() 即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个;另外 Subject.getSession(false),如果当前没有创建 Session 则返回 null(不过默认情况下如果启用会话存储功能的话在创建 Subject 时会主动创建一个 Session)。

session.getId();

获取当前会话的唯一标识。

session.getHost();

获取当前 Subject 的主机地址,该地址是通过 HostAuthenticationToken.getHost() 提供的。

session.getTimeout();
session.setTimeout(毫秒); 

获取 / 设置当前 Session 的过期时间;如果不设置默认是会话管理器的全局过期时间。

session.getStartTimestamp();
session.getLastAccessTime();


获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。

session.touch();
session.stop();

更新会话最后访问时间及销毁会话;当 Subject.logout() 时会自动调用 stop 方法来销毁会话。如果在 web 中,调用 javax.servlet.http.HttpSession. invalidate() 也会自动调用 Shiro Session.stop 方法进行销毁 Shiro 的会话。

session.setAttribute("key", "123");
session.removeAttribute("key");

设置 / 获取 / 删除会话属性;在整个会话范围内都可以对这些属性进行操作。

Shiro 提供的会话可以用于 JavaSE/JavaEE 环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。

测试

/**
     * 测试会话session
     */

@Test
public void testSession() {
    Subject subject = login("classpath:shiro-realm.ini", "zhang", "123");
    Session session = subject.getSession();
    //获取当前会话的唯一标识
    System.out.println(session.getId());
    //获取当前 Subject 的主机地址
    System.out.println(session.getHost());
    //获取 / 设置当前 Session 的过期时间
    System.out.println(session.getTimeout());
    // 获取会话的启动时间及最后访问时间
    System.out.println(session.getStartTimestamp());
    System.out.println(session.getLastAccessTime());
    // 更新会话最后访问时间及销毁会话
    //session.touch();
    //session.stop();
    //设置属性值
    session.setAttribute("name", "zhangsan");
    System.out.println(session.getAttribute("name"));

}

2.6.2.会话管理器

会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager,且提供了SessionsSecurityManager 实现直接把会话管理委托给相应的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了 SessionsSecurityManager。

SecurityManager 提供了如下接口:

Session start(SessionContext context); //启动会话
Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话

另外用于 Web 环境的 WebSessionManager 又提供了如下接口:

boolean isServletContainerSessions();// 是否使用 Servlet 容器的会话

Shiro 还提供了 ValidatingSessionManager 用于验资并过期会话:

void validateSessions();// 验证所有会话是否过期

7

DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;
DefaultWebSessionManager:用于 Web 环境的实现,可以替代 ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。

spring中配置


<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
    <property name="globalSessionTimeout" value="1800000"/>
    <property name="deleteInvalidSessions" value="true"/>
    <property name="sessionValidationSchedulerEnabled" value="true"/>
   	<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
    <property name="sessionDAO" ref="sessionDAO"/>
bean>

2.6.3.会话监听器

会话监听器用于监听会话创建、过期及停止事件:

public class MySessionListener1 implements SessionListener {
    @Override
    public void onStart(Session session) {//会话创建时触发
        System.out.println("会话创建:" + session.getId());
    }
    @Override
    public void onExpiration(Session session) {//会话过期时触发
        System.out.println("会话过期:" + session.getId());
    }
    @Override
    public void onStop(Session session) {//退出/会话过期时触发
        System.out.println("会话停止:" + session.getId());
    }  
}

如果只想监听某一个事件,可以继承 SessionListenerAdapter 实现:

public class MySessionListener2 extends SessionListenerAdapter {
    @Override
    public void onStart(Session session) {
        System.out.println("会话创建:" + session.getId());
    }
}

2.6.4.会话存储 / 持久化(sessionDAO)

Shiro 提供 SessionDAO 用于会话的 CRUD,即 DAO(Data Access Object)模式实现:

//如DefaultSessionManager在创建完session后会调用该方法;如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;返回会话ID;主要此处返回的ID.equals(session.getId());
Serializable create(Session session);
//根据会话ID获取会话
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
void update(Session session) throws UnknownSessionException;
//删除会话;当会话过期/会话停止(如用户退出时)会调用
void delete(Session session);
//获取当前所有活跃用户,如果用户量多此方法影响性能
Collection<Session> getActiveSessions();&nbsp;

Shiro 内嵌了如下 SessionDAO 实现:

8

AbstractSessionDAO 提供了 SessionDAO 的基础实现,如生成会话 ID 等;CachingSessionDAO 提供了对开发者透明的会话缓存的功能,只需要设置相应的 CacheManager 即可;MemorySessionDAO 直接在内存中进行会话维护;而 EnterpriseCacheSessionDAO 提供了缓存功能的会话维护,默认情况下使用 MapCache 实现,内部使用 ConcurrentHashMap 保存缓存的会话。

Shiro 提供了使用 Ehcache 进行会话存储,Ehcache 可以配合 TerraCotta 实现容器无关的分布式集群。

首先在 pom.xml 里添加如下依赖:

<dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-ehcacheartifactId>
    <version>1.2.2version>
dependency>

配置 ehcache.xml

<cache name="shiro-activeSessionCache"
       maxEntriesLocalHeap="10000"
       overflowToDisk="false"
       eternal="false"
       diskPersistent="false"
       timeToLiveSeconds="0"
       timeToIdleSeconds="0"
       statistics="true"/>

Cache 的名字默认为 shiro-activeSessionCache,即设置的 sessionDAO 的 activeSessionsCacheName 属性值。

spring中配置


<bean id="sessionIdGenerator"
      class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>

<bean id="sessionDAO"
      class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
    <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
    <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
bean>

<bean id="sessionValidationScheduler"
      class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
    <property name="sessionValidationInterval" value="1800000"/>
    <property name="sessionManager" ref="sessionManager"/>
bean>

<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
    <property name="globalSessionTimeout" value="1800000"/>
    <property name="deleteInvalidSessions" value="true"/>
    <property name="sessionValidationSchedulerEnabled" value="true"/>
    <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
    <property name="sessionDAO" ref="sessionDAO"/>
bean>
  • sessionDAO. activeSessionsCacheName:设置 Session 缓存名字,默认就是 shiro-activeSessionCache;
  • cacheManager:缓存管理器,用于管理缓存的,此处使用 Ehcache 实现;
  • cacheManager.cacheManagerConfigFile:设置 ehcache 缓存的配置文件;
  • securityManager.cacheManager:设置 SecurityManager 的 cacheManager,会自动设置实现了 CacheManagerAware 接口的相应对象,如 SessionDAO 的 cacheManager;

如果自定义实现 SessionDAO,继承 CachingSessionDAO 即可:

public class MySessionDAO extends CachingSessionDAO {
    private JdbcTemplate jdbcTemplate = JdbcTemplateUtils.jdbcTemplate();
     protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        String sql = "insert into sessions(id, session) values(?,?)";
        jdbcTemplate.update(sql, sessionId, SerializableUtils.serialize(session));
        return session.getId();
    }
protected void doUpdate(Session session) {
    if(session instanceof ValidatingSession && !((ValidatingSession)session).isValid()) {
        return; //如果会话过期/停止 没必要再更新了
    }
        String sql = "update sessions set session=? where id=?";
        jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId());
    }
    protected void doDelete(Session session) {
        String sql = "delete from sessions where id=?";
        jdbcTemplate.update(sql, session.getId());
    }
    protected Session doReadSession(Serializable sessionId) {
        String sql = "select session from sessions where id=?";
        List<String> sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId);
        if(sessionStrList.size() == 0) return null;
        return SerializableUtils.deserialize(sessionStrList.get(0));
    }
}

doCreate/doUpdate/doDelete/doReadSession 分别代表创建 / 修改 / 删除 / 读取会话;此处通过把会话序列化后存储到数据库实现。

2.7.实现rememberMe

Shiro 提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:

  1. 首先在登录页面选中 RememberMe 然后登录成功;如果是浏览器登录,一般会把 RememberMe 的 Cookie 写到客户端并保存下来;
  2. 关闭浏览器再重新打开;会发现浏览器还是记住你的;
  3. 访问一般的网页服务器端还是知道你是谁,且能正常访问;
  4. 但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。

2.7.1.RememberMe 配置

spring中配置


<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg value="rememberMe" />
    
    <property name="maxAge" value="604800" />
bean>
  • sessionIdCookie:maxAge=-1 表示浏览器关闭时失效此 Cookie;
  • rememberMeCookie:即记住我的 Cookie,保存时长 30 天;

<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
    <property name="cookie" ref="rememberMeCookie" />
bean>
  • rememberMe 管理器,cipherKey 是加密 rememberMe Cookie 的密钥;默认 AES 算法;

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="userRealm" />
    <property name="cacheManager" ref="cacheManager"/>
    <property name="rememberMeManager" ref="rememberMeManager"/>
bean>
  • web过滤器

    
        <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
            <property name="securityManager" ref="securityManager"/>
            <property name="loginUrl" value="/login/toLogin" />
            <property name="unauthorizedUrl" value="/error.html"/>
            <property name="filterChainDefinitions">
                <value>
                    /login/toLogin = anon
                    /login/checkLogin = anon
                    /login/logout = authc
                    /** = user
                value>
            property>
        bean>
    
anon:例子/admins/**=anon 没有参数,表示可以匿名使用。
authc:例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数
roles(角色):例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
perms(权限):例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString
        是你访问的url里的?后面的参数。
authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证
ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https
user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查

2.8.shiro异常处理

处理shiro异常有3种方式:
(1)使用Spring-MVC提供的SimpleMappingExceptionResolver;
(2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器;
(3)使用@ExceptionHandler注解实现异常处理;

2.8.1.使用Spring-MVC提供的SimpleMappingExceptionResolver;

代码若抛出授权未通过异常即UnauthorizedException,跳转noPermission.jsp页面(会经过视图解析器后跳转)


<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
            <prop key="org.apache.shiro.authz.UnauthorizedException">/noPermissionprop>
        props>
    property>
bean>

注:/noPermission 是指经过视图解析器的页面,这里直接配需要跳转到的页面的名字即可

2.8.2.实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器;

实现HandlerExceptionResolver 接口自定义异常处理器,HandlerExceptionResolver是一个接口,只有一个方法,我们只需要实现这个接口;

我们在springMVC的配置文件中进行如下配置


<bean id="exceptionResolver" class="net.wanho.exception.MyExceptionResolver">bean>

MyExceptionResolver

public class MyExceptionResolver implements HandlerExceptionResolver {

   public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
      System.out.println("==============异常开始=============");
         ex.printStackTrace();
      System.out.println("==============异常结束=============");
      ModelAndView mv = new ModelAndView("noPermission");
      return mv;
   }

}

2.8.3.@ExceptionHandler注解实现异常处理

将@ExceptionHandler标注在Controller的方法上,该方法将处理由@RequestMapping方法抛出的异常

首先要增加BaseController类,并在类中使用@ExceptionHandler注解声明异常处理,代码如下:

public class BaseController {
   /** 基于@ExceptionHandler异常处理 */
   @ExceptionHandler
   public String exp(HttpServletRequest request, Exception ex) {
      // 根据不同错误转向不同页面  
      if(ex instanceof org.apache.shiro.authz.UnauthorizedException) {
         return "noPermission";
      }else {
         return "error";
      }
   }

将需要异常处理的Controller继承BaseController即可。

更多资深讲师相关课程资料、学习笔记请入群后向管理员免费获取,更有专业知识答疑解惑。入群即送价值499元在线课程一份。
QQ群号:560819979
敲门砖(验证信息):雨打蕉

你可能感兴趣的:(java,深入浅出学java,ssm整合,shiro,框架,异常处理)