shiro安全框架

shiro安全框架


shiro 是什么

Apache Shiro 是 Java 的一个安全(权限)框架。它可以非常容易的开发出足够安全的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境 。

Shiro 可以完成:认证、授权、加密、会话管理、与Web 集成、缓存 等。下载:http://shiro.apache.org/ 或 https://github.com/apache/shiro

功能介绍

webp

Shiro目标:Shiro开发团队所称的“应用程序安全”的四个基石——身份验证、授权、会话管理和密码

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户 对某个资源是否具有某个权限;
  • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境,也可以是 Web 环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

还有额外的功能来支持和增强:

  • Web Support:Web 支持,可以非常容易的集成到Web 环境;
  • **Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
  • Concurrency:Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,就能把权限自动传播过去;
  • Testing:提供测试支持,测试支持的存在是为了帮助您编写单元测试和集成测试,确保您的代码将是安全的。
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

Shiro术语

  • Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。Subject 代表了当前“用户”
  • Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作。

Shiro架构

webp

从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成工作:

Application Code
Subject
SecurityManager
Realm
  • Subject(org.apache.shiro.subject.Subject):应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject。Subject 代表了当前“用户”, 这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫, 机器人等;与 Subject 的所有交互都会委托给 SecurityManager; Subject 其实是一个门面,SecurityManager 才是实际的执行者;
  • SecurityManager (org.apache.shiro.mgt.SecurityManager):安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;其管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中 DispatcherServlet 的角色
  • Realm (org.apache.shiro.realm.Realm):Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/ 权限进行验证用户是否能进行操作;可以有一个或多个Realm来自定义,我们在自定义的Realm中获得数据库中真实的用户名和密码,来与传入的用户名和密码进行比较。

从Shiro内部来看:

  • Authenticator(org.apache.shiro.authc.Authenticator):认证器,身份验证负责 Subject 认证,执行和对验证用户(登录),可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  • Authorizer(org.apache.shiro.authz.Authorizer):授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • SessionManager (org.apache.shiro.session.mgt.SessionManager):管理 Session 生命周期的组件;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境
  • SessionDAO:对 session 的 CURD 操作
  • CacheManager(org.apache.shiro.cache.CacheManager):缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据 基本上很少改变,放到缓存中后可以提高访问的性能
  • Cryptography (org.apache.shiro.crypto.*):密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密

shiro 的主要功能 - 身份认证

1 Subject 认证

身份认证就是在应用中谁能证明他就是他本人,一般会使用用户名和密码作为认证信息。

2 Subject 认证主体

Subject 认证主体包含两个信息:

  • Principals:身份,即用户名
  • Credentials:凭证,即密码
3 认证流程

webp

  1. 用户发送请求进行 Subject 认证(调用 subject.login(token))
  2. SecurityManager 会去 Authenticator(认证器)中查找相应的 Realms(可能不止一个)源
  3. Realms 可以根据不同类型的 Realm 中去查找用户信息,并进行判断是否认证成功
4 快速搭建 helloWorld

1.导包

<dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.12</version>
        </dependency>
    </dependencies>

2.创建 Realm /resources/shiro.ini

[users]
acey=123456
jack=111

3进行身份验证

public class HelloWorld {

    public static void main(String[] args) {
//        加载配置文件,初始化 SecurityManager 工厂
        Factory<SecurityManager> factory = new IniSecurityManagerFactory
          ("classpath:shiro.shiro.ini");
//        获取 SecurityManager 实例
        SecurityManager securityManager = factory.getInstance();
//        把 SecurityManager 绑定到 SecurityUtils 中
        SecurityUtils.setSecurityManager(securityManager);
//        得到当前执行的用户
        Subject currentUser = SecurityUtils.getSubject();
        
        //测试使用Session
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }
        
        if (!currentUser.isAuthenticated()) {
             //创建 token 令牌,用户名/密码
            UsernamePasswordToken token = new UsernamePasswordToken("acey", "123456");
            token.setRememberMe(true);
            try {
                 //登录
                currentUser.login(token);
                System.out.println("登录成功");
            } catch (UnknownAccountException uae) {
                //没有指定的账户
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
               //账户存在,密码不匹配
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
               //用户被锁定异常
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }
        
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //测试是否有某个角色
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //测试用户是否有某种权限
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //测试用户是否有某种权限
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //登出
        currentUser.logout();

        System.exit(0);
    }
}

shiro 的主要功能 - 授权

权限授权就是访问控制,在应用中控制谁能访问哪些资源

1 权限认证中的几个元素
  • 权限:即操作某个资源的权限,这些资源可以是某个链接,也可以是某个图片,也可以是对某个模块的数据的 CURL
  • 角色:即权限的集合,一个角色可以有多个权限
  • 用户:代表访问的用户,即  Subject
2 授权的流程

webp

  1. 当用户访问应用中的某个资源时,会被 SecurityManager 拦截.
  2. SecurityManager 会去调用 Authorizer(授权器)
  3. Authorizer 会根据 Subject 的身份去相应的 Realm 中去查找该 Subject 是否有权限去访问该资源
3 授权实现

1 导包
2 配置 permission(权限) resources/shiro_permission.ini

[main] 
authc.loginUrl=/login  //表示用户登录失败跳转到 /login
roles.unauthorrizedUrl=/unauthorrized.jsp //表示用户没有对应的访问角色跳转到/unauthorrized.jsp
perms.unauthorrizedUrl=/unauthorrized.jsp  //表示用户没有对应的访问权限跳转到/unauthorrized.jsp

[users]
acey=123456,role1,role2
jack=123,role1
[roles]
role1=user:select // role1 角色有访问 user:select 的权限
role2=user:add,/delete //role2 角色有访问 user:add 和 /delete 的权限

[urls]
/login=anon  //表示任何用户都可以访问 /login
/index=authc //表示只有身份认证通过的用户才可以访问 /index
/index=roles[role1,role2...] //表示只有用户含有 role1 role2 ... 角色才可以访问 /index
/index=perms["user:create","/update"]  //表示只有用户含有 "user:create" 
                      和"/update"权限才可以访问 /index 
/index?=authc //`?`通配符,表示一个字符,如/index1 /indexa /index- (不能匹配/index) ,
                      将符合这种规则的请求进行`authc`拦截
/index*=authc  `*`通配符,表示零个或一个或多个字符,如/index1213asd /index /index2 ,
                      将符合这种规则的请求进行`authc`拦截
/index/**=authc  `**`表示匹配零个或一个或多个路径,如/index/create /index/create/update/...  ,
                      将符合这种规则的请求进行`authc`拦截
/index*/**authc  可以匹配 /index12/create/update/...

3 配置 roles (角色) resources/shiro_role.ini

[users]
acey=123456,role1,role2 //表示有一个用户,用户名是acey,密码为123456,有role1和role2角色
jack=123,role1

4 验证用户角色是否足够

public class RoleTest {

//  使用 checkRole 来检验角色时,若权限不足会返回 false
    @Test
    public void testHasRole() {
        Subject currentUser= ShiroUtil.login("classpath:shiro_role.ini", "acey", "123456");
        // Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "jack", "123");
        System.out.println(currentUser.hasRole("role1")?"has role1":"has not role1");
        currentUser.logout();
    }

    //  使用 checkRole 来检验角色时,若权限不足会抛出异常
    @Test
    public void testCheckRole() {
        Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "acey", "123456");
        // Subject currentUser=ShiroUtil.login("classpath:shiro_role.ini", "jack", "123");
        currentUser.checkRole("role1");

        currentUser.logout();
    }
}

5 验证用户权限是否足够

public class PermissionTest {

//  使用 checkPermission 来检验权限时,若权限不足会返回 false
    @Test
    public void testIsPermitted() {
        Subject currentUser= ShiroUtil.login("classpath:shiro_permission.ini", "acey", "123456");
        System.out.println(currentUser.isPermitted("user:select")?"has user:select":"hsa not user:select");

        currentUser.logout();
    }

//  使用 checkPermission 来检验权限时,若权限不足会抛出异常
    @Test
    public void testCheckPermitted() {
        Subject currentUser=ShiroUtil.login("classpath:shiro_permission.ini", "acey", "123456");
        // Subject currentUser=ShiroUtil.login("classpath:shiro_permission.ini", "jack", "123");
        currentUser.checkPermission("user:select");
        currentUser.logout();
    }
}

ssm 和 shiro 整合

导入依赖
2)配置 web.xml(shiro过滤器)

 <!-- shiro过滤器定义 -->
    <filter>  
        <filter-name>shiroFilter</filter-name>  
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>  
    <init-param>  
    <!-- 该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->  
    <param-name>targetFilterLifecycle</param-name>  
    <param-value>true</param-value>  
    </init-param>  
    </filter>  
    <filter-mapping>  
            <filter-name>shiroFilter</filter-name>  
            <url-pattern>/*</url-pattern>  
    </filter-mapping>

3)编写自己的 Realm(一般权限都是从数据库中查找,所以需要自定义)

public class MyRealm extends AuthorizingRealm{

    @Resource
    private UserService userService;
    
    /**
     * 为当前登录的用户授予角色和权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //获取用户名
        String userName=(String)principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
        //进行授权角色
        authorizationInfo.setRoles(userService.getRoles(userName));
        //进行授权权限
        authorizationInfo.setStringPermissions(userService.getPermissions(userName));
        return authorizationInfo;
    }

    /**
     *验证当前登录的用户
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String userName=(String)token.getPrincipal();
        //根据用户名查找用户信息
            User user=userService.getByUserName(userName);
            if(user!=null){
                AuthenticationInfo authcInfo=new SimpleAuthenticationInfo(user.getUserName(),user.getPassword(),getName());
                return authcInfo;
            }else{
                return null;                
            }
    }
}

4)spring 和 shiro 配置整合

<!-- 自定义Realm -->
    <bean id="myRealm" class="com.acey.realm.MyRealm"/>
    
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">  
      <property name="realm" ref="myRealm"/>  
    </bean>  
    
    <!-- Shiro过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
        <!-- Shiro的核心安全接口,这个属性是必须的 -->  
        <property name="securityManager" ref="securityManager"/>
        <!-- 身份认证失败,则跳转到登录页面的配置 -->  
        <property name="loginUrl" value="/index.jsp"/>
        <!-- 权限认证失败,则跳转到指定页面 -->  
        <property name="unauthorizedUrl" value="/unauthor.jsp"/>  
        <!-- Shiro连接约束配置,即过滤链的定义 -->  
        <property name="filterChainDefinitions">  
            <value>  
                 /login=anon
                /admin*=authc
                /student=roles[teacher]
                /teacher=perms["user:create"]
            </value>  
        </property>
    </bean>  
    
    <!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->  
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>  
    
    <!-- 开启Shiro注解 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>  
        <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">  
      <property name="securityManager" ref="securityManager"/>  
    </bean>  

一般角色和权限都存在数据库中,所以我们还可以自定义一个 filter 去自己验证每一个请求的 Subject 是否有权限去访问,这样我们就可以减少对过滤链的维护.比如

<!-- Shiro过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
        <!-- Shiro的核心安全接口,这个属性是必须的 -->  
        <property name="securityManager" ref="securityManager"/>
        <!-- 身份认证失败,则跳转到登录页面的配置 -->  
        <property name="loginUrl" value="/index.jsp"/>
        <!-- 权限认证失败,则跳转到指定页面 -->  
        <property name="unauthorizedUrl" value="/unauthor.jsp"/>  
        <!-- Shiro连接约束配置,即过滤链的定义 -->  
        <property name="filterChainDefinitions">  
            <value>  
                 /login=anon
                /admin*=authc
                /student=roles[teacher]
                /teacher=perms["user:create"]
            </value>  
        </property>
    </bean>  

可以改成

<!-- Shiro过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
        <!-- Shiro的核心安全接口,这个属性是必须的 -->  
        <property name="securityManager" ref="securityManager"/>
        <!-- 身份认证失败,则跳转到登录页面的配置 -->  
        <property name="loginUrl" value="/index.jsp"/>
        <!-- 权限认证失败,则跳转到指定页面 -->  
        <property name="unauthorizedUrl" value="/unauthor.jsp"/>  
        <property name="ownFilter" class="ownFilter.class">
        <!-- Shiro连接约束配置,即过滤链的定义 -->  
        <property name="filterChainDefinitions">  
            <value>  
                 /login=anon
               /**=ownFilter
            </value>  
        </property>
    </bean>  

AuthenticationToken简介

AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)。
Shiro会调用 CredentialsMatcher 对象的 doCredentialsMatch 方法
对 AuthenticationInfo对象和 AuthenticationToken 进行匹配。
匹配成功则表示主体(Subject)认证成功,否则表示认证失败。

public interface AuthenticationToken extends Serializable {
   Object getPrincipal(); //身份
   Object getCredentials(); //凭据
}

public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
   //用于实现基于用户名/密码主体(Subject)身份认证。可以实现“记住我”及“主机验证”的支持。
   private String username;
   private char[] password;
   private boolean rememberMe;
   private String host;
   ...
}

public interface HostAuthenticationToken extends AuthenticationToken {
   String getHost();// 获取用户“主机”
}

public interface RememberMeAuthenticationToken extends AuthenticationToken {
   boolean isRememberMe();// 记住我
}
  • 总结:
    一般情况下UsernamePasswordToken已经可以满足我们的大我数需求。
    当我们遇到需要声明自己的Token类时,
    可以根据需求来实现AuthenticationToken,HostAuthenticationToken或RememberMeAuthenticationToken。
  1. 如果不需要“记住我”,也不需要“主机验证”,则可以实现AuthenticationToken;
  2. 如果需要“记住我”,则可以实现RememberMeAuthenticationToken;
  3. 如果需要“主机验证”功能,则可以实现HostAuthenticationToken;
  4. 如果需要“记住我”,且需要“主机验证”,则可以像UsernamePasswordToken一样,同时实现RememberMeAuthenticationToken和HostAuthenticationToken。
  5. 如果需要其他自定义功能,则需要自己实现。

SecurityUtils

  • 在 Shiro 中 SecurityUtils 是一个抽象类。并且没有任何子类。在其中声明了一个静态属性,三个静态方法。
public abstract class SecurityUtils {
    // 用来存储当前应用中全局唯一的一个SecurityManager。
    private static SecurityManager securityManager;

    public SecurityUtils() {
    }

    //Shiro 中最核心的方法了,用来获取 Subject.
    public static Subject getSubject() {
        Subject subject = ThreadContext.getSubject();
        if (subject == null) {
            subject = (new Builder()).buildSubject();
            ThreadContext.bind(subject);
        }

        return subject;
    }

    public static void setSecurityManager(SecurityManager securityManager) {
        securityManager = securityManager;
    }

    public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException {
        SecurityManager securityManager = ThreadContext.getSecurityManager();
        if (securityManager == null) {
            securityManager = securityManager;
        }

        if (securityManager == null) {
            String msg = "No SecurityManager accessible to the calling code, either bound to the " + ThreadContext.class.getName() + " or as a vm static singleton.  This is an invalid application configuration.";
            throw new UnavailableSecurityManagerException(msg);
        } else {
            return securityManager;
        }
    }
}

实现认证Realm

public class ShiroRealm extends AuthorizingRealm {
    /**
	 * 身份认证 - 之后走下面的 授权
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
  //1.把AuthenticationToken转换为UsernamePasswordToken
		UsernamePasswordToken tokenInfo = (UsernamePasswordToken) authenticationToken;
		// 2. 获取用户输入的账号
		String telephone = tokenInfo.getUsername();
		// 获取用户输入的密码
		String password = String.valueOf(tokenInfo.getPassword());

		// 通过telephone从数据库中查找 User对象,如果找到进行验证
		// 这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
		if (StringUtils.isEmpty(telephone)) {
			// 用户账号未空的情况
			return null;
		}
    //3从数据库获取 账号对应用户信息
		SystemUserDO user = systemUserService.getUserByTelephone(telephone);
		//4 判断账号是否存在
		if (user == null) {
			//返回null -> shiro就会知道这是用户不存在的异常
			return null;
			//throw new UnknownAccountException();
		}

		// 5验证密码 【注:这里不采用shiro自身密码验证 , 采用的话会导致用户登录密码错误时,已登录的账号也会自动下线!】
		if (!MD5Util.checkPassword(password, user.getPassword())) {
			throw new IncorrectCredentialsException("密码错误!");
		}

		// 6判断账号是否被冻结
		if (user.getStatus() == null || user.getStatus().equals(1)) {
			throw new LockedAccountException("用户账号已被冻结,禁止使用!");
		}

//7 根据用户情况,构建AuthenticationInfo 对象并返回
		//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以在此判断或自定义实现
		/**
		 * 进行验证 -> 注:shiro会自动验证密码
		 * 参数1:principal -> 放对象就可以在页面任意地方拿到该对象里面的值
		 * 参数2:hashedCredentials -> 密码
		 * 参数3:realmName -> 自定义的Realm
		 */
		SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName());

		// 如果用户已经存在登录信息,踢除用户
		if (!StringUtils.isBlank(user.getToken())) {
			ShiroUtils.deleteLoginInfo(user.getToken(), true);
		}

		// 验证成功开始踢人(清除缓存和Session)
//		ShiroUtils.deleteCache(telephone, true);
		// 认证成功后更新token
		String token = ShiroUtils.getSession().getId().toString();
		user.setToken(token);
		user.setLastLoginDate(new Date());
		user.setLastLoginDateStamp(user.getLastLoginDate().getTime());
		user.setLastLoginIp(IPUtils.getRealIP(request));

//		systemUserDAO.updateById(user);
		systemUserService.save(user, true);
		return authenticationInfo;
	}
}

密码的MD5加密

方案一

自定义shiro的Realm实现和CredentialsMatcher实现以及Token实现 - 蓝萝卜blu - 博客园

方案二

自己封装一个MD5加密工具

自定义ShiroRealm extends AuthorizingRealm中身份认证时,将登录输入的密码和数据库中保存的密码进行比对,当然数据库中的密码是用户创建时就加密存入数据库的。

public class AuthenticationUtils {

	/**
	 * 生成密码
	 *
	 * @param text 明文密码
	 * @return 加密过的密码
	 */
	public static String generatePassword(String text) {
		return MD5Util.createMD5Str(text);
	}

	/**
	 * 检查密码是否一致
	 *
	 * @param external 外部输入的密码
	 * @param password 系统中记录的加密密码
	 * @return true|false
	 */
	public static boolean checkPassword(String external, String password) {
		String externalPassword = generatePassword(external.toUpperCase());
		if (externalPassword.equals(password.toUpperCase())) {
			return true;
		}
		return false;
	}
}
public class MD5Util {
	/**
	 * 加密算法
	 **/
	public final static String HASH_ALGORITHM_NAME = "MD5";
	/**
	 * 循环次数
	 **/
	public final static int HASH_ITERATIONS = 1;

	public static String createMD5Str(String textPassword) {
		/**
		 * 加密算法,原文,盐值,循环次数
		 */
		SimpleHash hash = new SimpleHash(
				"MD5",
				textPassword,
				null,
				HASH_ITERATIONS);
		return hash.toString().toUpperCase();
	}
}

//源码分析
class ShiroRealm extends AuthorizingRealm,class AuthorizingRealm extends 。AuthenticatingRealm中有个CredentialsMatcher比对器,是个接口,重写doCredentialsMatch方法来达到自定义比对方法

public interface CredentialsMatcher {
    boolean doCredentialsMatch(AuthenticationToken var1, AuthenticationInfo var2);
}
public class SimpleCredentialsMatcher extends CodecSupport implements CredentialsMatcher {
...
}

最后分析源码得知,实现在HashedCredentialsMatcher类中,这里只贴出部分关键代码

public class HashedCredentialsMatcher extends SimpleCredentialsMatcher {
  public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        Object tokenHashedCredentials = this.hashProvidedCredentials(token, info);
        Object accountCredentials = this.getCredentials(info);
        return this.equals(tokenHashedCredentials, accountCredentials);
    }

    protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
        Object salt = null;
        if (info instanceof SaltedAuthenticationInfo) {
            salt = ((SaltedAuthenticationInfo)info).getCredentialsSalt();
        } else if (this.isHashSalted()) {
            salt = this.getSalt(token);
        }

        return this.hashProvidedCredentials(token.getCredentials(), salt, this.getHashIterations());
    }
    protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
        String hashAlgorithmName = this.assertHashAlgorithmName();
        return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
    }

}

核心加密方法就是SimpleHash

public class SimpleHash extends AbstractHash {
/**
  *algorithmName加密方法名
  *source 原文
  *salt 盐值
  *hashIterations  循环计算次数
  */
public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations) throws CodecException, UnknownAlgorithmException {
        this.hexEncoded = null;
        this.base64Encoded = null;
        if (!StringUtils.hasText(algorithmName)) {
            throw new NullPointerException("algorithmName argument cannot be null or empty.");
        } else {
            this.algorithmName = algorithmName;
            this.iterations = Math.max(1, hashIterations);
            ByteSource saltBytes = null;
            if (salt != null) {
                saltBytes = this.convertSaltToBytes(salt);
                this.salt = saltBytes;
            }

            ByteSource sourceBytes = this.convertSourceToBytes(source);
            this.hash(sourceBytes, saltBytes, hashIterations);
        }
    }
}

例如Md5加密

public class Md5Hash extends SimpleHash {
    public static final String ALGORITHM_NAME = "MD5";

    public Md5Hash() {
        super("MD5");
    }

    public Md5Hash(Object source) {
        super("MD5", source);
    }

    public Md5Hash(Object source, Object salt) {
        super("MD5", source, salt);
    }

    public Md5Hash(Object source, Object salt, int hashIterations) {
        super("MD5", source, salt, hashIterations);
    }

    public static Md5Hash fromHexString(String hex) {
        Md5Hash hash = new Md5Hash();
        hash.setBytes(Hex.decode(hex));
        return hash;
    }

    public static Md5Hash fromBase64String(String base64) {
        Md5Hash hash = new Md5Hash();
        hash.setBytes(Base64.decode(base64));
        return hash;
    }
}

看到这里就可以直接自定义一个加密工具了

public class MD5Util {
	/**
	 * 加密算法
	 **/
	public final static String HASH_ALGORITHM_NAME = "MD5";
	/**
	 * 循环次数
	 **/
	public final static int HASH_ITERATIONS = 1;

/**
	 * 生成密码
	 *
	 * @param textPassword 明文密码
	 * @return 加密过的密码
	 */
	public static String createMD5Str(String textPassword) {
		/**
		 * 加密算法,原文,盐值,循环次数
		 */
		SimpleHash hash = new SimpleHash(
				"MD5",
				textPassword,
				null,
				HASH_ITERATIONS);
		return hash.toString().toUpperCase();
	}
  
  /**
	 * 检查密码是否一致
	 *
	 * @param external 外部输入的密码
	 * @param password 系统中记录的加密密码
	 * @return true|false
	 */
	public static boolean checkPassword(String external, String password) {
		String externalPassword = createMD5Str(external.toUpperCase());
		if (externalPassword.equals(password.toUpperCase())) {
			return true;
		}
		return false;
	}

}

有一个问题,如果两户账户密码一样,那加密得到的结果也一样,如何避免这种情况,提高密码的安全性呢?加点作料,加点盐。

认证策略 AuthenticationStrategy

/*
*
*/
package org.apache.shiro.authc.pam;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.realm.Realm;
import java.util.Collection;

/**
*
* @see AllSuccessfulStrategy
* @see AtLeastOneSuccessfulStrategy
* @see FirstSuccessfulStrategy
* @since 0.2
*/
public interface AuthenticationStrategy {

   /**
    */
   AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException;

   /**
   *
    */
   AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;

   /**
    *
    */
   AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)
           throws AuthenticationException;

   /**
    *
    */
   AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException;
}

AuthenticationStrategy 是个无状态的组件,在认证过程中会进行4次调用。
① 在所有Realm被调用之前
②在调用Realm的getAuthenticationInfo方法之前
③在调用Realm的getAuthenticationInfo 方法之后
④在所有Realm被调用之后

Shiro有3中认证策略的具体实现 AuthenticationStrategy类:

  • AtLeastOneSuccessfulStrategy(默认)
    只要一个或者多个Realm认证通过,则整体身份认证就会视为成功。
  • FirstSuccessfulStrategy
    只有第一个验证通过,才会视为整体认证通过。其他的会被忽略。
  • AllSuccessfulStrategy

只有所有的Realm认证成功,才会被视为认证通过
自定义策略:继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy。

Realm顺序对认证是有影响的。

spring-shiro.xml 配置 认证策略:


<!-- Shiro默认会使用Servlet容器的Session,可通过sessionMode属性来指定使用Shiro原生Session -->
<!-- 这里主要是设置自定义的单Realm应用,若有多个Realm,可使用'realms'属性代替 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="cacheManager" ref="cacheManager"/>
     <!--<property name="realm" ref="myRealm"/>-->
     
    <property name="authenticator" ref="authenticator"/>
</bean>

<!--多个realm 配置-->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    <!--配置认证策略-->
    <property name="authenticationStrategy" ref="allSuccessfulStrategy"/>
    <property name="realms">
        <list>
            <ref bean="firstRealm"/>
            <ref bean="secondRealm"/>
        </list>
    </property>
</bean>
<!--全部通过-->
<bean id="allSuccessfulStrategy" class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
<!--只有第一个验证通过,才会视为整体认证通过。其他的会被忽略。-->
<bean id="firstSuccessfulStrategy" class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy"/>
<!--默认-->
<bean id="atLeastOneSuccessfulStrategy" class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"/>

授权

授权的概念
  • 授权:也叫做访问控制,即在应用中控制谁能访问哪些资源(如访问页面、编辑数据、页面操作等)。在授权中需要了解几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。

  • 主体:即访问应用的用户,在Shiro中使用Subject代表用户。用户只有授权后才允许访问相应的资源

  • 资源:在应用中用户可以访问的任何东西都称为资源。用户只有授权后才能访问。

  • 权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权利。即权限表示在应用中用户能不能访问某个资源。shiro支持粗颗粒度权限(如用户模块的所有权限)和细颗粒度权限(操作某个用户的权限,即实例级别的)。

  • 角色:角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限。不同的角色拥有一组不同的权限。

  • 隐式角色:即直接通过角色来验证用户有没有操作权限。比如:在应用中班长和课代表可以使用打印机,但是某一天老师不允许课代表使用打印机了,我们就需要将应用中课代表操作打印机的权限代码删除。即粒度是以角色为单位进行访问控制的,粒度较粗。

  • 显示角色:在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合;这样假设某个角色不能访问某个资源,只需要从角色代表的权限集合中移除即可;无需修改多处代码;即粒度是以资源、实例为单位的,粒度较细。

授权流程

解释:

  • 首先调用Subject.isPermitted*/hasRole*接口,其会自动委托给SecurityManager,而SecurityManager会接着委托给Authorizer;
  • Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通过PermissionResolver把真正的字符串转换成相应的Permission实例;
  • 在进行授权之前,其会调用相应的Realm获取Subject相应的角色、权限用于匹配传入的角色、权限;
  • Authorizer会判断Realm的角色、权限是否和传入的匹配,如果有多个Realm,或委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole*会返回true,否则返回false表示授权失败。

ModularRealmAuthorizer进行多Realm匹配流程:

  • 首先检查相应的Realm是否实现了Authorizer;
  • 如果实现了Authorizer,那么接着调用其相应的isPermitted*/hasRole*接口进行匹配;
  • 如果有一个Realm匹配那么僵返回true,否则返回false;

如果Realm进行首选的话,应该继承AuthorizingRealm,其流程是:

  • 如果调用hasRole*,则直接获取AuthorizationInfo.getRoles()与传入的角色比较即可
  • 如果调用如isPermitted(“user:view”),首先通过PermissionResolver将权限字符串转换成相应的Permission实例,默认使用WildcardPermissionResolver,即转换为通配符的WildcardPermission;
  • 通过AuthorizationInfo.getObjectPermissions()得到Permission实例集合;通过AuthorizationInfo.getStringPermissions()得到字符串集合并通过PermissionResolver解析为Permission实例;然后获取用户的角色,并通过RolePermissionResolver解析角色对应的权限集合(默认没有实现,可以自己提供);
  • 接着滴啊用Permission.implies(Permission p)逐个与传入的权限比较,如果有匹配的则返回true,否则返回false.
授权方式

Shiro支持三种方式的授权:

编程式: 通过写if/else授权代码块完成:

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")){
    //有权限
} else{
    //无权限
}

注解式: 通过在指定的Java方法上放置相应的注解完成:

@RequiresRoles("admin")
public void hello(){
    //有权限
}

没有权限将抛出相应的异常。

JSP标签: 在JSP页面通过相应的标签完成:

<shiro:hasRole name="admin">
    <!-- 有权限 -->
</shiro:hasRole>
实现授权
基于角色

基于角色的访问控制(隐式角色)

1、shiro-role.ini

[users]
tycoding=123,role1,role2

补充
此处ini配置文件的规则:用户名=密码,角色1,角色2,…。如果在需要在应用中判断用户是否拥有相应角色,就需要需要在相应的Realm中返回角色信息,也就是说Shiro不负责维护用户-角色信息,Shiro只是提供了相应的接口方便验证。
2、RoleTest.java

@Test
public void testHasRole() {

    //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
    Factory factory = new IniSecurityManagerFactory("classpath:shiro-role.ini");

    //2、得到SecurityManager实例,并绑定给SecurityUtils
    SecurityManager securityManager = (SecurityManager) factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);

    //3、得到Subject及创建用户名、密码身份的Token
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("tycoding", "123");

    try {
        //4、登录,即身份验证
        subject.login(token);

        //判断用户是否拥有角色:role1
        System.out.println(subject.hasRole("role1"));

        //判断用户是否拥有角色:role1、role2
        boolean[] check1 = subject.hasRoles(Arrays.asList("role1", "role2"));
        for (boolean b: check1) {
            System.out.println(b);
        }

        //判断用户是否拥有角色:role1、role2、role3
        boolean[] check2 = subject.hasRoles(Arrays.asList("role1", "role2", "role3"));
        for (boolean b: check2) {
            System.out.println(b);
        }

    } catch (AuthenticationException e) {
        //5、身份验证失败
        e.printStackTrace();
    }

    //6、退出
    subject.logout();
}

打印:

true
true

true
true
false

4、总结
如上就是基于角色的访问控制(即隐式角色),这种方式的缺点如果很多地方都进行了角色的判断,但是某一天不需要了,就要把相关的代码删除掉;这就是粗颗粒度造成的问题。

基于资源

基于资源的访问控制(显示角色)

1、shiro-permission.ini

[users]
tycoding=123,role1,role2

[roles]
role1:user:create,user:update
role2:user:create,user:delete

补充
此处ini配置文件的规则:”用户名=密码,角色1,角色2” “角色=权限1,权限2”。即首先根据用户名找到角色,然后再根据角色找到权限;即角色是权限的集合;Shiro同样不进行权限的维护,需要我们通过Realm返回相应的权限信息。只需要维护”用户-角色”之间的关系即可。

2、RoleTest.java

@Test
public void testPermissionRole() {
    //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
    Factory factory = new IniSecurityManagerFactory("classpath:shiro-permission.ini");

    //2、得到SecurityManager实例,并绑定给SecurityUtils
    SecurityManager securityManager = (SecurityManager) factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);

    //3、得到Subject及创建用户名、密码身份的Token
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("tycoding", "123");

    try {
        //4、登录,即身份验证
        subject.login(token);

        //判断用户是否拥有权限:user:create
        System.out.println(subject.isPermitted("user:create"));

        //潘墩用户是否拥有权限:user:update和user:delete
        boolean[] check = subject.isPermitted("user:create", "user:delete");
        for (boolean b: check) {
            System.out.println(b);
        }

    } catch (AuthenticationException e) {
        //5、身份验证失败
        e.printStackTrace();
    }

    //6、退出
    subject.logout();
}

3、打印结果:

true

true
true

4、总结
如上,我们事先了基于资源的访问控制(显示角色)。这种方式的优势显而易见,主要体现角色是权限的集合,这种方式的规则主要是资源标识符:操作,即是资源级别的粒度。如果我们需要更改某个角色的权限,只需要一个资源级别的修改,不会对其他模块代码产生影响,粒度小。需要维护用户–角色,角色–权限之间的关系。

Realm

  • 为什么要重写realm?
    因为在web开发中,我们需要realm内的真实数据是我们自己查出来的数据库中的数据,包含我们自己的逻辑,所以我们需要重写realm以存放我们的数据
  • 如何去重写realm?
    //Realm 继承体系
    Realm
    CachingRealm //支持数据缓存
    AuthenticatingRealm //支持缓存/认证
    AuthorizingRealm //支持缓存/认证/授权

ShiroFilterFactoryBean

  • Shiro提供了与Web集成的支持,其通过一个ShiroFilter入口来拦截需要安全控制的URL,然后进行相应的控制

AccessControlFilter

  • 是shiro-web模块当中比较重要的类,所有的拦截器都继承此类
  • shiro-web 提供的filter,每种filter都对应了不同的权限拦截规则
shiro枚举过滤器
public enum DefaultFilter {
//匿名拦截器,即不需要登录即可访问,一般用于静态资源
anon(AnonymousFilter.class),
//基于表单的拦截器,没登录就跳转到指定登录页面
authc(FormAuthenticationFilter.class),
//Http身份验证拦截器,
authcBasic(BasicHttpAuthenticationFilter.class),
authcBearer(BearerHttpAuthenticationFilter.class),
ip(IpFilter.class),
//退出拦截器
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
//权限授权拦截器,验证用户是否拥有所有权限 
perms(PermissionsAuthorizationFilter.class),
//端口拦截器,
port(PortFilter.class),
//rest风格拦截器,自动根据请求方法构建权限字符串
rest(HttpMethodPermissionFilter.class),
//角色授权,验证用户是否拥有角色
roles(RolesAuthorizationFilter.class),
//SSL拦截器,只有请求协议是https才能通过,否则会自动跳转https端口
ssl(SslFilter.class),
//用户拦截器,用户已经身份验证/记住我登录的都可
user(UserFilter.class),
invalidRequest(InvalidRequestFilter.class);
 }
  • AccessControlFilter继承至 ServletContextSupport
/**
	 * 开启Shiro-aop注解支持:使用代理方式所以需要开启代码支持
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
		return authorizationAttributeSourceAdvisor;
	}

Shiro 标签

权限注解:

@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。

Shiro 会话管理

Shiro 会话管理_w3cschool

也可以用redis做session缓存

参考:
https://www.w3cschool.cn/shiro/co4m1if2.html
https://blog.csdn.net/shitianhang123/article/details/84942117

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值