360SDN.COM

首页/Java/列表

用Shiro保护SpringMVC项目

来源:雪暖晴岚  2017-09-11 11:17:57    评论:0点击:

前言:在目前的项目中(基于SpringMVC+MySql),登录验证以及权限方面的管理一直是空白的,登录验证只是简单的把密码和数据库的密码取出来手动进行比较,而且没有权限控制,存在一些越权访问的情况。为此,准备着手改进项目的权限验证。对比了Spring Security和Spring Shiro方案,普遍觉得Shiro更容易上手,而且能满足大多数情况,并且之前有过部分Shiro基础,所以决定采用Shiro升级项目权限。

 

  • Shiro简介

Shiro主页:https://shiro.apache.org/index.html。

  1. Shiro能做什么

    Shiro简言之就是三个单词:Simple,Java,Security。更通俗一点的理解是,Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理功能,可为任何应用提供安全保障 - 从命令行应用、移动应用到大型网络及企业应用。

  2. Shiro是如何做到的

    Shiro的架构主要有三个顶级概念:Subject,SecurityManager和Realms。图1-1中展示了这些组件间的交互,接下来详细介绍:

图1-1 Shiro简易结构图

通过图1-1可以清楚的知道Shiro的基本架构,但是Shiro不止上述三个组件,其详细的其体系结构如下:

图1-2 Shiro体系结构

       结合上图重点理解下上图中的各个组件(英文是官网的释义):

1)Subject:A security-specific ‘view’of the entity (user, 3rd-party service, cron job, etc) currently interactingwith the software.对照上图的第一层,Subject可以理解为“当前的操作用户”。但是称之为“用户”并不准确,因为“用户”一词通常跟人相关。在安全领域,术语“Subject”可以是人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。

2)Security ManagerAs mentioned above, theSecurityManager is the heart of Shiro’s architecture. It is mostly an‘umbrella’ object that coordinates its managed components to ensure they worksmoothly together. It also manages Shiro’s view of every application user, soit knows how to perform security operations per user. Subject的“幕后”推手是SecurityManager。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。它是Shiro框架的核心,起协调内部各组件的作用。同时管理Shiro的所有用户,可以进行用户相关的安全操作。

3) AuthenticatorThe Authenticator is the component that is responsible for executingand reacting to authentication (log-in) attempts by users. When a user tries tolog-in, that logic is executed by the Authenticator. The Authenticator knowshow to coordinate with one or more Realms that store relevant user/accountinformation. The data obtained from these Realms is used to verify the user’sidentity to guarantee the user really is who they say they are.Authenticator是用于执行用户的认证行为的组件,当用户登录时,登录的逻辑操作由Authenticator执行。Authenticator知道如何与存储用户/帐号信息的一个或多个Realms协作。从这些Realms获取的数据用于验证用户的真实身份。Authentication Strategy:如果配置了不止一个Realm,AuthenticationStrategy将用来决定什么样的条件下认证会成功或失败(比如,如果一个realm的数据认证成功而其他的却失败了,能算是认证通过吗?还是必须所有的都认证成功?还是只要一个就可以?)。

4)AuthorizerThe Authorizer is thecomponent responsible determining users’ access control in the application. Itis the mechanism that ultimately says if a user is allowed to do something ornot. Like the Authenticator, the Authorizer also knows how to coordinate withmultiple back-end data sources to access role and permission information. TheAuthorizer uses this information to determine exactly if a user is allowed toperform a given action. Authorizer用于控制程序中用户的访问,最终决定用户是否被允许做什么。和Authenticator一样, Authorizer也会与底层的数据源交互来获取角色与权限信息,从而准确控制用户的行为。

5)SessionManagerThe SessionManager knowshow to create and manage user Session lifecycles to provide a robust Sessionexperience for users in all environments. This is a unique feature in the worldof security frameworks - Shiro has the ability to natively manage user Sessionsin any environment, even if there is no Web/Servlet or EJB container available.By default, Shiro will use an existing session mechanism if available, (e.g.Servlet Container), but if there isn’t one, such as in a standalone applicationor non-web environment, it will use its built-in enterprise session managementto offer the same programming experience. The SessionDAO exists to allow anydatasource to be used to persist sessions. SessionManager可以创建用户会话(Session)维护其生命周期并在所有环境中提供了强大的用户体验。Shiro可以管理任何环境下的用户会话,即使是非Web/Servlet或EJB容器下——这是区别于其他安全框架而特有的功能。默认情况下,Shiro会使用(容器)提供的会话管理机制,但如果没有,比如在独立的程序或非Web环境中,Shiro将使用它内置的企业会话管理功能以提供一致的编程体验。SessionDAO可使用各种数据源来存储会话。

SessionDAOSessionDAO替SessionManager执行会话的存储操作(CRUD)。任何的数据存储都可以接入到会话管理中。

6)CacheManagerThe CacheManager createsand manages Cache instance lifecycles used by other Shiro components. BecauseShiro can access many back-end data sources for authentication, authorizationand session management, caching has always been a first-class architecturalfeature in the framework to improve performance while using these data sources.Any of the modern open-source and/or enterprise caching products can be pluggedin to Shiro to provide a fast and efficient user-experience. CacheManager创建与管理缓存的实例与生命周期以供Shiro中的其他组件使用。因为在Shiro中可以使用多个数据源用于认证、授权和会话管理,而缓存可以在使用这些数据源时改善性能,所以也就成为框架中很重要的一个功能。Shiro中可以使用任何开源或商业的缓存方案。

7)Cryptography Cryptography is a naturaladdition to an enterprise security framework. Shiro’s crypto package containseasy-to-use and understand representations of crytographic Ciphers, Hashes (akadigests) and different codec implementations. All of the classes in thispackage are carefully designed to be very easy to use and easy to understand.Anyone who has used Java’s native cryptography support knows it can be achallenging animal to tame. Shiro’s crypto APIs simplify the complicated Javamechanisms and make cryptography easy to use for normal mortal human beings.CacheManager创建与管理缓存的实例与生命周期以供Shiro中的其他组件使用。因为在Shiro中可以使用多个数据源用于认证、授权和会话管理,而缓存可以在使用这些数据源时改善性能,所以也就成为框架中很重要的一个功能。Shiro中可以使用任何开源或商业的缓存方案。

8)RealmsAs mentioned above, Realmsact as the ‘bridge’ or ‘connector’ between Shiro and your application’ssecurity data. When it comes time to actually interact with security-relateddata like user accounts to perform authentication (login) and authorization(access control), Shiro looks up many of these things from one or more Realmsconfigured for an application. You can configure as many Realms as you need(usually one per data source) and Shiro will coordinate with them as necessaryfor both authentication and authorization.如上所知,Realm在shiro与应用安全数据之间扮演“桥梁”/“连接器”的角色。每当因为执行认证或授权操作而需要像用户帐号等安全相关的数据时,shiro会从程序配置的一个或多个Realm中查询。你可以根据需要配置多个Realm。

但将如此多功能包含在一个组件中,同时又要做到灵活可定制还是挺困难的。为了简化配置实现灵活的可插拔性,Shiro的实现在设计上高度模块化——甚至SecurityManager(及其继承类)基本上都不用做太多事了,相反,SecurityManager更多的是作为一个轻量“容器(container)”,将功能的实现委托给内部组件。这种“封装器”的设计在上面的架构图中已有体现。当组件执行逻辑的时候,SecurityManager知道如何以及何时去协调组件做出正确的行为。

SecurityManager和 JavaBean 兼容,这允许你(或者配置途径)通过标准的JavaBean 访问/设置方法(get/set)很容易地定制插件。Shiro 模块可以根据用户行为转化成简易的配置。

  • 如何使用Shiro

上面之所以对Shiro有这么多的介绍,因为官网上的定义能让我们对Shiro这个框架有更深入的理解,再结合我们实际的例子,才能算真正的用过Shiro。

先来梳理一下,我们大概知道了Shiro是如何运作的,简言之就是:用户输入用户名、密码->获取当前Subject->去对应的Relam中进行校验->调用Authenticator中写好的方法->返回验证结果->验证成功,进行相关缓存操作。所以我的第一步其实是先实现自定义的Relam,然后配置Shiro,最后再login操作模块中添加Shiro验证过程。

  • 自定义Relam

重点是要实现Relam中的两个方法,具体见代码:

public class RelamFilter extends AuthorizingRealm {
   
@Autowired
   
private UserService userService;

   
@Override
   
protected AuthorizationInfo doGetAuthorizationInfo
(PrincipalCollection principalCollection) {
       
//获取登录时输入的用户名
       
String loginName = (String) principalCollection
.fromRealm(getName()).iterator().next()
;
       
//到数据库查是否有此对象
       
User user = userService.getUserByAccount(loginName);
        if
(user != null) {
           
if(user.getU_available() != 1){
               
throw new AuthenticationException
                               (
"该账号已被禁用");
           
}
           
//权限信息对象info,用来存放查出的用户的
                       所有的角色(
role)及权限(permission
           
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
           
//用户的角色集合
           
info.addRole(user.getRole());
         
return info;
       
}
       
return null;
   
}


   
@Override
   
protected AuthenticationInfo doGetAuthenticationInfo
(AuthenticationToken authenticationToken) throws AuthenticationException {
       
//UsernamePasswordToken对象用来存放提交的登录信息
       
UsernamePasswordToken token = (UsernamePasswordToken)
authenticationToken
;
       
//查出是否有此用户
       
User user = userService.getUserByAccount(token.getUsername());
        if
(user != null) {
           
if(user.getU_available() != 1){
                
throw new AuthenticationException("该账号已被禁用");
           
}
           
//若存在,将此用户存放到登录认证info
           
return new
SimpleAuthenticationInfo(user.getU_account(), user.getU_password(), getName());
       
}
       
return null;
   
}

   
public UserService getUserService() {
       
return userService;
   
}

   
public void setUserService(UserService userService) {
       
this.userService = userService;
   
}
}


  • 配置Shiro

首先是web.xml中添加Shiro相关配置:

<context-param>
  <param-name>
contextConfigLocation</param-name>
  <param-value>
classpath:spring-mybatis.xml,
classpath:spring-shiro.xml
</param-value>
</context-param>
<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>

然后在项目的resources目录下新建sprin-shiro.xml文件,配置如下:

<!-- 启用shrio授权注解拦截方式 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property
name="securityManager" ref="securityManager"/>
    <property
name="loginUrl" value="/index"/>
    <property
name="successUrl" value="/main"/>
    <property
name="unauthorizedUrl" value="/error/unAuthorized"/>
    <property
name="filterChainDefinitions">
        <value>
           
<!--* @see   /admin=authc,roles[admin]      表示用户必需已通过认证,并拥有admin角色才可以正常发起'/admin'请求-->
            <!--* @see   /edit=authc,perms[admin:edit] 
表示用户必需已通过认证,并拥有admin:edit权限才可以正常发起'/edit'请求-->
            <!--* @see   /home=user                    
表示用户不一定需要已经通过认证,只需要曾经被Shiro记住过登录状态就可以正常发起'/home'请求-->
           
/index=anon
            /main=authc
       
</value>
    </property>
</bean>
<!-- 配置进行授权和认证的 Realm -->
<bean id="myRealm" class="com.canghailongyin.filter.RelamFilter">
    <property
name="userService" ref="userService" />
</bean>
<bean
id="userService" class="com.canghailongyin.service.impl.UserServiceImpl" />
<!-- 配置 Shiro SecurityManager Bean. -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property
name="realm" ref="myRealm"/>
</bean>
<!-- 配置 Bean 后置处理器: 会自动的调用和 Spring 整合后各个组件的生命周期方法. -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- 指定本系统SESSIONID, 默认为: JSESSIONID 问题: SERVLET容器名冲突, JETTY, TOMCAT 等默认JSESSIONID,
当跳出SHIRO SERVLET时如ERROR-PAGE容器会为JSESSIONID重新分配值导致登录会话丢失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <constructor-arg
name="name" value="timetable.session.id"/>
</bean>

这里有几点需要注意:1. filterChainDefinitions的配置详情请参考:https://shiro.apache.org/web.html#Web-FilterChainDefinitions。2.MyRelam指向上面自定义的Relam类,其中userService是根据用户名获取User对象的接口。

最后在在spring-mvc.xml中添加如下bean:

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property
name="exceptionMappings">
        <props>
            <prop
key="org.apache.shiro.authz.UnauthorizedException">error/unAuthorized</prop>
            <prop
key="java.lang.Throwable">error/500</prop>
        </props>
    </property>
</bean>

因为在实际项目中,发现配置的未授权页面无法访问,所以加上这个。

  • 登录Controller修改

从如下代码可以清晰的看出,将username和password传给UsernamePasswordToken后,获取到当前Subject,然后调用login方法即去验证MyRelam中的doGetAuthenticationInfo方法。

/**
     *
用户登录
    
*/
   
@RequestMapping(value="/login", method= RequestMethod.POST)
   
public String login(HttpServletRequest request, HttpServletResponse response){
        JSONObject flag =
new JSONObject();
       
String verifyCode = request.getParameter("verifyCode");
       
HttpSession session = request.getSession();
       
String code = (String)session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
       
//首先校验验证码
       
if(!verifyCode.equals(code))
        {
            flag.put(
"flag",false);
           
flag.put("error","验证码输入有误");
           
ResponseUtils.setResponseHeaders(response);
           
ResponseUtils.sendJSONData(response, flag.toString());
            return null;
       
}

        String username = request.getParameter(
"username");
       
String password = request.getParameter("password");
       
password = MD5.md5(password);

       
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
       
token.setRememberMe(true);
       
System.out.println("为了验证登录用户而封装的token" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));
       
//获取当前的Subject
       
Subject currentUser = SecurityUtils.getSubject();
        try
{
           
//在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
           
//每个Realm都能在必要时对提交的AuthenticationTokens作出反应
           
//所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
           
System.out.println("对用户[" + username + "]进行登录验证..验证开始");
           
currentUser.login(token);
           
System.out.println("对用户[" + username + "]进行登录验证..验证通过");
       
}catch(UnknownAccountException uae){
            System.
out.println("对用户[" + username + "]进行登录验证..验证未通过,未知账户");
           
flag.put("flag",false);
           
flag.put("error","账户名不存在");
       
}catch(IncorrectCredentialsException ice){
            System.
out.println("对用户[" + username + "]进行登录验证..验证未通过,错误的凭证");
           
flag.put("flag",false);
           
flag.put("error","密码错误");
       
}catch(LockedAccountException lae){
            System.
out.println("对用户[" + username + "]进行登录验证..验证未通过,账户已锁定");
           
flag.put("flag",false);
           
flag.put("error","账户已锁定");
       
}catch(ExcessiveAttemptsException eae){
            System.
out.println("对用户[" + username + "]进行登录验证..验证未通过,错误次数过多");
           
flag.put("flag",false);
           
flag.put("error","密码错误次数过多");
       
}catch(AuthenticationException ae){
           
//通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
           
System.out.println("对用户[" + username + "]进行登录验证..验证未通过,堆栈轨迹如下");
            
ae.printStackTrace();
           
flag.put("flag",false);
           
flag.put("error",ae.getMessage());
       
}
       
//验证是否登录成功
       
if(currentUser.isAuthenticated()){
//            Session session = currentUser.getSession();
            //
Session进行管理
           
flag.put("flag",true);
           
System.out.println("用户[" + username + "]登录认证通过(这里可以进行一些认证通过后的一些系统参数初始化操作)");
       
}else{
            token.clear()
;
            if
(!flag.containsKey("flag")){
                flag.put(
"flag",false);
           
}
           
if(!flag.containsKey("error")){
                flag.put(
"error","登陆认证错误");
           
}
        }

        ResponseUtils.setResponseHeaders(response)
;
       
ResponseUtils.sendJSONData(response, flag.toString());
        return null;
   
}

以上就是部署Shiro过程中的步骤。

  • Shiro小结

    Shiro提供了很完整的权限校验功能,而且可以是基于url、基于资源等不同方式的验证,在实际项目过程中,有了Shiro支撑,相信可以满足我们很多对资源控制的需求。

阅读原文

为您推荐

友情链接 |九搜汽车网 |手机ok生活信息网|ok生活信息网|ok微生活
 Powered by www.360SDN.COM   京ICP备11022651号-4 © 2012-2016 版权