shiro无状态web集成的示例代码

在一些环境中,可能需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时带上相应的用户名进行登录。如一些REST风格的API,如果不使用OAuth2协议,就可以使用如REST+HMAC认证进行访问。HMAC(Hash-based Message Authentication Code):基于散列的消息认证码,使用一个密钥和一个消息作为输入,生成它们的消息摘要。注意该密钥只有客户端和服务端知道,其他第三方是不知道的。访问时使用该消息摘要进行传播,服务端然后对该消息摘要进行验证。如果只传递用户名+密码的消息摘要,一旦被别人捕获可能会重复使用该摘要进行认证。解决办法如:

1、每次客户端申请一个Token,然后使用该Token进行加密,而该Token是一次性的,即只能用一次;有点类似于OAuth2的Token机制,但是简单些;

2、客户端每次生成一个唯一的Token,然后使用该Token加密,这样服务器端记录下这些Token,如果之前用过就认为是非法请求。

为了简单,本文直接对请求的数据(即全部请求的参数)生成消息摘要,即无法篡改数据,但是可能被别人窃取而能多次调用。解决办法如上所示。

服务器端

对于服务器端,不生成会话,而是每次请求时带上用户身份进行认证。

服务控制器

@RestController
public class ServiceController {
  @RequestMapping("/hello")
  public String hello1(String[] param1, String param2) {
    return "hello" + param1[0] + param1[1] + param2;
  }
} 

当访问/hello服务时,需要传入param1、param2两个请求参数。

加密工具类

com.github.zhangkaitao.shiro.chapter20.codec.HmacSHA256Utils:

//使用指定的密码对内容生成消息摘要(散列值)
public static String digest(String key, String content);
//使用指定的密码对整个Map的内容生成消息摘要(散列值)
public static String digest(String key, Map<String, ?> map)  

对Map生成消息摘要主要用于对客户端/服务器端来回传递的参数生成消息摘要。

Subject工厂  

public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
  public Subject createSubject(SubjectContext context) {
    //不创建session
    context.setSessionCreationEnabled(false);
    return super.createSubject(context);
  }
}  

通过调用context.setSessionCreationEnabled(false)表示不创建会话;如果之后调用Subject.getSession()将抛出DisabledSessionException异常。

StatelessAuthcFilter

类似于FormAuthenticationFilter,但是根据当前请求上下文信息每次请求时都要登录的认证过滤器。

public class StatelessAuthcFilter extends AccessControlFilter {
 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
   return false;
 }
 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  //1、客户端生成的消息摘要
  String clientDigest = request.getParameter(Constants.PARAM_DIGEST);
  //2、客户端传入的用户身份
String username = request.getParameter(Constants.PARAM_USERNAME);
  //3、客户端请求的参数列表
  Map<String, String[]> params =
   new HashMap<String, String[]>(request.getParameterMap());
  params.remove(Constants.PARAM_DIGEST);
  //4、生成无状态Token
  StatelessToken token = new StatelessToken(username, params, clientDigest);
  try {
   //5、委托给Realm进行登录
   getSubject(request, response).login(token);
  } catch (Exception e) {
   e.printStackTrace();
   onLoginFail(response); //6、登录失败
   return false;
  }
  return true;
 }
 //登录失败时默认返回401状态码
 private void onLoginFail(ServletResponse response) throws IOException {
  HttpServletResponse httpResponse = (HttpServletResponse) response;
  httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
  httpResponse.getWriter().write("login error");
 }
}

获取客户端传入的用户名、请求参数、消息摘要,生成StatelessToken;然后交给相应的Realm进行认证。

StatelessToken   

public class StatelessToken implements AuthenticationToken {
  private String username;
  private Map<String, ?> params;
  private String clientDigest;
  //省略部分代码
  public Object getPrincipal() { return username;}
  public Object getCredentials() { return clientDigest;}
}  

用户身份即用户名;凭证即客户端传入的消息摘要。

StatelessRealm 

用于认证的Realm。

public class StatelessRealm extends AuthorizingRealm {
  public boolean supports(AuthenticationToken token) {
    //仅支持StatelessToken类型的Token
    return token instanceof StatelessToken;
  }
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    //根据用户名查找角色,请根据需求实现
    String username = (String) principals.getPrimaryPrincipal();
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    authorizationInfo.addRole("admin");
    return authorizationInfo;
  }
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    StatelessToken statelessToken = (StatelessToken) token;
    String username = statelessToken.getUsername();
    String key = getKey(username);//根据用户名获取密钥(和客户端的一样)
    //在服务器端生成客户端参数消息摘要
    String serverDigest = HmacSHA256Utils.digest(key, statelessToken.getParams());
    //然后进行客户端消息摘要和服务器端消息摘要的匹配
    return new SimpleAuthenticationInfo(
        username,
        serverDigest,
        getName());
  } 

  private String getKey(String username) {//得到密钥,此处硬编码一个
    if("admin".equals(username)) {
      return "dadadswdewq2ewdwqdwadsadasd";
    }
    return null;
  }
}

此处首先根据客户端传入的用户名获取相应的密钥,然后使用密钥对请求参数生成服务器端的消息摘要;然后与客户端的消息摘要进行匹配;如果匹配说明是合法客户端传入的;否则是非法的。这种方式是有漏洞的,一旦别人获取到该请求,可以重复请求;可以考虑之前介绍的解决方案。

Spring配置——spring-config-shiro.xml

<!-- Realm实现 -->
<bean id="statelessRealm"
 class="com.github.zhangkaitao.shiro.chapter20.realm.StatelessRealm">
  <property name="cachingEnabled" value="false"/>
</bean>
<!-- Subject工厂 -->
<bean id="subjectFactory"
 class="com.github.zhangkaitao.shiro.chapter20.mgt.StatelessDefaultSubjectFactory"/>
<!-- 会话管理器 -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
  <property name="sessionValidationSchedulerEnabled" value="false"/>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
  <property name="realm" ref="statelessRealm"/>
  <property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled"
   value="false"/>
  <property name="subjectFactory" ref="subjectFactory"/>
  <property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
  <property name="staticMethod"
   value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
  <property name="arguments" ref="securityManager"/>
</bean>

sessionManager通过sessionValidationSchedulerEnabled禁用掉会话调度器,因为我们禁用掉了会话,所以没必要再定期过期会话了。

<bean id="statelessAuthcFilter"
  class="com.github.zhangkaitao.shiro.chapter20.filter.StatelessAuthcFilter"/>

每次请求进行认证的拦截器。

<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  <property name="securityManager" ref="securityManager"/>
  <property name="filters">
    <util:map>
      <entry key="statelessAuthc" value-ref="statelessAuthcFilter"/>
    </util:map>
  </property>
  <property name="filterChainDefinitions">
    <value>
      /**=statelessAuthc
    </value>
  </property>
</bean> 

所有请求都将走statelessAuthc拦截器进行认证。

其他配置请参考源代码。

客户端

此处使用SpringMVC提供的RestTemplate进行测试。

此处为了方便,使用内嵌jetty服务器启动服务端:

public class ClientTest {
  private static Server server;
  private RestTemplate restTemplate = new RestTemplate();
  @BeforeClass
  public static void beforeClass() throws Exception {
    //创建一个server
    server = new Server(8080);
    WebAppContext context = new WebAppContext();
    String webapp = "shiro-example-chapter20/src/main/webapp";
    context.setDescriptor(webapp + "/WEB-INF/web.xml"); //指定web.xml配置文件
    context.setResourceBase(webapp); //指定webapp目录
    context.setContextPath("/");
    context.setParentLoaderPriority(true);
    server.setHandler(context);
    server.start();
  }
  @AfterClass
  public static void afterClass() throws Exception {
    server.stop(); //当测试结束时停止服务器
  }
}

在整个测试开始之前开启服务器,整个测试结束时关闭服务器。

测试成功情况

@Test
public void testServiceHelloSuccess() {
  String username = "admin";
  String param11 = "param11";
  String param12 = "param12";
  String param2 = "param2";
  String key = "dadadswdewq2ewdwqdwadsadasd";
  MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
  params.add(Constants.PARAM_USERNAME, username);
  params.add("param1", param11);
  params.add("param1", param12);
  params.add("param2", param2);
  params.add(Constants.PARAM_DIGEST, HmacSHA256Utils.digest(key, params));
  String url = UriComponentsBuilder
      .fromHttpUrl("http://localhost:8080/hello")
      .queryParams(params).build().toUriString();
   ResponseEntity responseEntity = restTemplate.getForEntity(url, String.class);
  Assert.assertEquals("hello" + param11 + param12 + param2, responseEntity.getBody());
}

对请求参数生成消息摘要后带到参数中传递给服务器端,服务器端验证通过后访问相应服务,然后返回数据。

测试失败情况

@Test
public void testServiceHelloFail() {
  String username = "admin";
  String param11 = "param11";
  String param12 = "param12";
  String param2 = "param2";
  String key = "dadadswdewq2ewdwqdwadsadasd";
  MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
  params.add(Constants.PARAM_USERNAME, username);
  params.add("param1", param11);
  params.add("param1", param12);
  params.add("param2", param2);
  params.add(Constants.PARAM_DIGEST, HmacSHA256Utils.digest(key, params));
  params.set("param2", param2 + "1"); 

  String url = UriComponentsBuilder
      .fromHttpUrl("http://localhost:8080/hello")
      .queryParams(params).build().toUriString();
  try {
    ResponseEntity responseEntity = restTemplate.getForEntity(url, String.class);
  } catch (HttpClientErrorException e) {
    Assert.assertEquals(HttpStatus.UNAUTHORIZED, e.getStatusCode());
    Assert.assertEquals("login error", e.getResponseBodyAsString());
  }
}

在生成请求参数消息摘要后,篡改了参数内容,服务器端接收后进行重新生成消息摘要发现不一样,报401错误状态码。

到此,整个测试完成了,需要注意的是,为了安全性,请考虑本文开始介绍的相应解决方案。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • shiro无状态web集成的示例代码

    在一些环境中,可能需要把Web应用做成无状态的,即服务器端无状态,就是说服务器端不会存储像会话这种东西,而是每次请求时带上相应的用户名进行登录.如一些REST风格的API,如果不使用OAuth2协议,就可以使用如REST+HMAC认证进行访问.HMAC(Hash-based Message Authentication Code):基于散列的消息认证码,使用一个密钥和一个消息作为输入,生成它们的消息摘要.注意该密钥只有客户端和服务端知道,其他第三方是不知道的.访问时使用该消息摘要进行传播,服务端

  • Android 实现无网络页面切换的示例代码

    本文介绍了Android 实现无网络页面切换的示例代码,分享给大家,具体如下: 实现思路 需求是在无网络的时候显示特定的页面,想到要替换页面的地方,大多都是recyclerview或者第三方recyclerview这种需要显示数据的地方,因此决定替换掉页面中所有的recyclerview为无网络页面 实现过程 1 在BaseActivity中,当加载布局成功以后,通过id找到要替换的view,通过indexOfChild()方法,找到要替换的view的位置,再通过remove和add view来

  • Android 实现无网络传输文件的示例代码

    最近的项目需要实现一个 Android 手机之间无网络传输文件的功能,就发现了 Wifi P2P(Wifi点对点)这么一个功能,最后也实现了通过 Wifi 隔空传输文件 的功能,这里我也来整理下代码,分享给大家. Wifi P2P 是在 Android 4.0 以及更高版本系统中加入的功能,通过 Wifi P2P 可以在不连接网络的情况下,直接与配对的设备进行数据交换.相对于蓝牙,Wifi P2P 的搜索速度和传输速度更快,传输距离更远 实现的效果如下所示: 客户端.png 服务器端.png 一

  • 详解Nginx如何配置Web服务器的示例代码

    概述 今天主要分享怎么将NGINX配置作为Web服务器,并包括以下部分: 设置虚拟服务器 配置位置 使用变量 返回特定状态码 重写HTTP响应 在高层次上,将NGINX配置作为Web服务器有一些问题需要了解,定义它处理哪些URL以及如何处理这些URL上的资源的HTTP请求. 在较低层次上,配置定义了一组控制对特定域或IP地址的请求的处理的虚拟服务器. 用于HTTP流量的每个虚拟服务器定义了称为位置的特殊配置实例,它们控制特定URI集合的处理. 每个位置定义了自己的映射到此位置的请求发生的情况.

  • spring boot 与kafka集成的示例代码

    新建spring boot项目 这里使用intellij IDEA 添加kafka集成maven <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLoc

  • Jenkins与SVN持续集成的示例代码

    概述 Jenkins是一个Java语言编写的开源的持续集成工具,它的前身为Hudson,使用它可以进行项目的自动编译.测试与发布,这极大的减轻了团队之间的繁琐重复的工作,从而加快了整个项目的交付进度. 官网下载Jenkins&SVN&eclipse,版本号没要求,建议使用最新稳定版本 登录Jenkins:http://localhost:8080 登录SVN:http://localhost:3343/csvn 默认admin账号登录SVN,登录后,点击版本库->创建版本库 4.打开

  • Node.js搭建WEB服务器的示例代码

    前言 这几天为了熟悉vue.js框架,还有webpack的使用,就准备搭建一个发布和浏览markdwon的简单WEB应用.原本是想着用bash脚本和busybox的httpd来作为后台服务,但是bash脚本解析和生成JSON非常不方便,而用Java语言写又觉得部署不方便,所以就想到了正在用到的Node.js,于是就有了这篇博文.(文末有本文代码的github地址) 简单例子 首先,从搭建最简单的 Hello world 开始,建立以下目录.文件和内容. 建立项目及运行 project web-s

  • Spring Boot与React集成的示例代码

    前言 前不久学习了Web开发,用React写了前端,Spring Boot搭建了后端,然而没有成功地把两个工程结合起来,造成前端与后端之间需要跨域通信,带来了一些额外的工作. 这一次成功地将前端工程与后端结合在一个Project中,记录一下,也希望能帮到那些和我一样的入门小白. 环境 Windows 10 - x64, Java 1.8.0, node v8.9.4, npm 6.1.0 前奏 *JDK, Node 和 NPM请自行安装 新建一个Spring Boot工程 在Intellij里选

  • Vue+Express实现登录状态权限验证的示例代码

    前提 对Vue全家桶有基本的认知. 用有node环境 了解express 另外本篇只是介绍登录状态的权限验证,以及登录,注销的前后端交互.具体流程(例如:前端布局,后端密码验证等).以后有时间再对这些边边角角进行补充 一丶业务分析 1.什么情况下进行权限验证? 访问敏感接口 前端向后端敏感接口发送ajax 后端进行session验证,并返回信息 前端axios拦截返回信息,根据返回信息进行操作 进行页面切换 页面切换,触发vue-router的路由守卫 路由守卫根据跳转地址进行验证,如需权限,则

  • spring boot整合Shiro实现单点登录的示例代码

    Shiro是什么 Shiro是一个Java平台的开源权限框架,用于认证和访问授权.具体来说,满足对如下元素的支持: 用户,角色,权限(仅仅是操作权限,数据权限必须与业务需求紧密结合),资源(url). 用户分配角色,角色定义权限. 访问授权时支持角色或者权限,并且支持多级的权限定义. Q:对组的支持? A:shiro默认不支持对组设置权限. Q:是否可以满足对组进行角色分配的需求? A:扩展Realm,可以支持对组进行分配角色,其实就是给该组下的所有用户分配权限. Q:对数据权限的支持? 在业务

随机推荐