/spring-authorization-manager

基于 Spring MVC,提供 API 服务端的身份验证功能。通过 Redis、MySQL 维护登录用户与分配 Token 的映射关系。

Primary LanguageJava

Spring Authorization Manager

为Api服务端添加简单的Token鉴权功能,基于Spring MVC

功能简述

  1. 对每个请求进行身份验证,如果身份验证失败直接返回错误信息(可以自定义错误信息和Http状态码)
  2. 通过鉴权信息获得当前登录的用户,并自动注入到Controller的方法中

使用方法

仓库:

<repository>
    <id>scienjus-mvn-repo</id>
    <url>https://raw.github.com/ScienJus/maven/snapshot/</url>
    <snapshots>
        <enabled>true</enabled>
        <updatePolicy>always</updatePolicy>
    </snapshots>
</repository>

依赖:

<dependency>
    <groupId>com.scienjus</groupId>
    <artifactId>spring-authorization-manager</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

所有依赖库,相信大部分已经存在于你的项目中了:

<dependencies>
  <dependency>
	  <groupId>com.scienjus</groupId>
	  <artifactId>spring-authorization-manager</artifactId>
	  <version>1.0-SNAPSHOT</version>
  </dependency>
  <!--Spring MVC依赖-->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
  </dependency>
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
  </dependency>
  <!--Redis依赖,只有在使用RedisTokenManager时才需要-->
  <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
  </dependency>
  <!--数据库依赖,只有在使用MySQLTokenManager时才需要-->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
  </dependency>
  <dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
  </dependency>
</dependencies>

使用Redis存储Token

将Jedis客户端注入到RedisTokenManager

<!--Redis配置-->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
</bean>

<!--Redis连接池-->
<bean id = "jedisPool" class="redis.clients.jedis.JedisPool">
  <constructor-arg index="0" ref="jedisPoolConfig"/>
  <constructor-arg index="1" value="${redis.host}"/>
  <constructor-arg index="2" value="${redis.port}" type="int"/>
  <constructor-arg index="3" value="${redis.timeout}" type="int"/>
  <constructor-arg index="4" value="${redis.password}"/>
</bean>

<!--管理验证信息的bean-->
<bean id="tokenManager" class="com.scienjus.authorization.manager.impl.RedisTokenManager">
       <!--Token失效时间-->
       <property name="tokenExpireSeconds" value="3600" />
       <!--Redis客户端-->
       <property name="jedisPool" ref="jedisPool" />
</bean>

使用MySQL存储Token

只需要将RedisTokenManager替换成MySQLTokenManager,并将数据源注入进去:

<!--数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
       <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
       <property name="url" value="jdbc:mysql://127.0.0.1:3306/demo"/>
       <property name="username" value="root"/>
       <property name="password" value="root"/>
</bean>

<!--管理验证信息的bean-->
<bean id="tokenManager" class="com.scienjus.authorization.manager.impl.DBTokenManager">
       <!--Token失效时间-->
       <property name="tokenExpireSeconds" value="3600" />
       <!--数据源-->
       <property name="dataSource" ref="dataSource" />
       <!--存储验证信息的表名-->
       <property name="tableName" value="users" />
       <!--存储Key的字段名-->
       <property name="keyColumnName" value="username" />
       <!--存储Token的字段名-->
       <property name="tokenColumnName" value="token" />
       <!--存储Token过期时间的字段名-->
       <property name="expireAtColumnName" value="expire_at" />
</bean>

配置身份验证的拦截器

将配置好的TokenManager注入到AuthorizationInterceptor中:

<mvc:interceptors>
       <!--身份验证的拦截器-->
       <bean id="authorizationInterceptor" class="com.scienjus.authorization.interceptor.AuthorizationInterceptor">
              <!--验证信息存储的Http头-->
              <property name="httpHeaderName" value="authorization" />
              <!--验证信息通用前缀,例如Bearer-->
              <property name="httpHeaderPrefix" value="" />
              <!--验证失败时的错误信息-->
              <property name="unauthorizedErrorMessage" value="令牌失效,请重新登录" />
              <!--管理验证信息的bean-->
              <property name="manager" ref="tokenManager" />
       </bean>
</mvc:interceptors>

接下来只需要对需要身份验证的方法加上@Authorization注解即可,例如:

@RestController
@RequestMapping("/home")
public class TokenController {

    @RequestMapping(method = RequestMethod.GET)
    @Authorization
    public ResponseEntity<String> home() {
        return new ResponseEntity<>("Hello World", HttpStatus.OK);
    }

}

也可以直接在Controller类上加上该注解,这将会使该Controller中的所有方法都需要进行身份验证。

配置获得当前登录用户的解析器

首先需要实现UserModelRepository接口的getCurrentUser方法,可以通过Key得到对应的用户对象,然后配置一个解析器,并将其注入到CurrentUserMethodArgumentResolver

<mvc:annotation-driven>
       <mvc:argument-resolvers>
              <!--配置注入登录用户的解析器-->
              <bean id="currentUserMethodArgumentResolver" class="com.scienjus.authorization.resolvers.CurrentUserMethodArgumentResolver">
                     <!--需要解析的用户类-->
                     <property name="userModelClass" value="com.scienjus.domain.User" />
                     <!--查询用户的bean-->
                     <property name="userModelRepository" ref="userRepository" />
              </bean>
       </mvc:argument-resolvers>
</mvc:annotation-driven>

<!--通过Key获得对应用户的bean-->
<bean id="userRepository" class="com.scienjus.repository.UserRepository" />

然后只需要在方法的参数上添加一个用户对象,并加上@CurrentUser注解,例如:

@RestController
@RequestMapping("/home")
public class TokenController {

    @RequestMapping(method = RequestMethod.GET)
    @Authorization
    public ResponseEntity<String> home(@CurrentUser user) {
        return new ResponseEntity<>("Hello " + user.getUsername(), HttpStatus.OK);
    }

}

需要注意的是,拥有@CurrentUser参数的方法,可以没有@Authorization注解,此时如果请求未登录,该参数会为null

但是如果想要使用CurrentUserMethodArgumentResolver则必须配置AuthorizationInterceptor

###更新日志

2016-3-1

增加了泛型约束

2015-11-27

修改了拦截器的部分代码,内容为:

  1. 将返回鉴权失败信息的输出流从response.getWriter改为了response.getOutputStream,因为@ResponseBody默认也是用的后者,便于统一监控返回内容。
  2. 可以通过配置文件自定义鉴权失败的http状态码了,默认为401(unauthorized)。
  3. 将返回鉴权失败的Content-Type设置为application/json了,否则可能会导致iOS的网络库AFNetWorking解析报错。

###帮助

如果您在使用中遇到了问题,可以给我提 Issues,或是通过邮件联系我,我的邮箱是:i@scienjus.com

源码分析见我的这篇博客

一个简单的Demo