Jersey spring oauth 2.0

December 8, 2017
jersey 2 jersey restful oauth2.0 junit

主题

覆盖以下几个内容 :
1. jersey 2开rest服务,之前有jersey 1的示例
2. jersey 2与spring 4的集成
3. jersey 2的http式启动,之前有相关示例
4. restful的oauth2.0机制实现,仿微信登录
5. jersey的统一异常委托处理,之前有相关介绍
6. rest包的DynamicFeature、ContainerRequestFilter等扩展和应用
7. jUnit启闭rest http server
8. hibernate部分略

目录

文件结构
1.jersey开rest服务/jUnit启闭rest http server
2.容器里的rest服务
3.统一的异常处理
4.oauth2.0实现

文件结构

一、java文件结构

- serv
	- ApplicationResConfigure.java  配置rest加载项
	- AuthFilter.java rest拦截器
	- DynamicRolesDynamicFeature.java 使rest拦截器动态加载  
- serv.dao
	- AuthDao.java rest服务类-auth2.0相关
	- UserDao.java rest服务类-user相关
- serv.exception
	- AuthException.java 自定义异常处理类
	- MyException.java 自定义异常处理类-根类
	- MyExceptionMapper.java 统一异常处理
	- ExceptionBean.java 自定义异常VO
- serv.po
	- AuthInfo.java oauth授权信息
	- User.java 用户信息
- serv.rest
	- AuthServ.java restful服务接口类-oauth2.0相关
	- UserServ.java restful服务接口类-用户相关
- serv.service
	- AuthService.java 服务类-oauth2.0相关
- serv.tools
	- code.java 定义返回的json的code字段的值 
	- CommConst.java 一些常量
	- FluzzyMap.java 缓存oauth2.0的code到map(后期应该考虑用其它缓存形式)

二、资源文件结构

- src/main/resources	
	- spring-context.xml spring的基础配置文件
- webapp
	- WEB-INF
		- web.xml web工程的配置
	- pom.xml maven依赖管理

三、测试相关

- src.test.java
	- serv.test
		- WebApp.java 以java Main启动http server
		- RestTest.java main启动并模拟http请求

1.jersey开rest服务/jUnit启闭rest http server

添加基本的依赖

<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/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.icwant</groupId>
  <artifactId>xltx_service</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  
   <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jersey-version>2.6</jersey-version>
        <spring-version>4.3.12.RELEASE</spring-version>
        <grizzly-version>2.3.10</grizzly-version>
        <servlet-api-version>3.0.1</servlet-api-version>
        <icwant-version>0.0.1-SNAPSHOT</icwant-version>
    </properties>
    
    <dependencies>
		<dependency>
            <groupId>org.glassfish.grizzly</groupId>
            <artifactId>grizzly-http-servlet</artifactId>
            <version>${grizzly-version}</version>
        </dependency>
        
        <dependency>
			<groupId>org.glassfish.jersey.media</groupId>
			<artifactId>jersey-media-json-jackson</artifactId>
			<version>${jersey-version}</version>
		</dependency>
  
  		<dependency>
    		<groupId>javax.servlet</groupId>
    		<artifactId>javax.servlet-api</artifactId>
    		<version>${servlet-api-version}</version>
    		<scope>provided</scope>
		</dependency>
				
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.9</version>
            <scope>test</scope>
        </dependency>
        
        
        <dependency>
			<groupId>org.glassfish.jersey.ext</groupId>
			<artifactId>jersey-spring3</artifactId>
			<version>${jersey-version}</version>
			<exclusions>
				<exclusion>
					<groupId>org.springframework</groupId>
					<artifactId>spring-core</artifactId>
				</exclusion>
				<exclusion>
					<groupId>org.springframework</groupId>
					<artifactId>spring-web</artifactId>
				</exclusion>
				<exclusion>
					<groupId>org.springframework</groupId>
					<artifactId>spring-beans</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		
		<!-- mysql驱动包 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.13</version>
		</dependency>
		
		<!-- spring-hibernate中间件 -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-orm</artifactId>
			<version>${spring-version}</version>
		</dependency>
		
		<!-- Spring dependencies -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
			<version>${spring-version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring-version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
			<version>${spring-version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${spring-version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring-version}</version>
		</dependency>
		
    </dependencies>
    
   <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <inherited>true</inherited>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
       </plugins>
   </build>
  
</project>

启动http server:

package serv.test;

import java.io.IOException;

import javax.servlet.ServletRegistration;

import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.NetworkListener;
import org.glassfish.grizzly.servlet.WebappContext;
import org.glassfish.jersey.servlet.ServletContainer;

public class WebApp {
	
	public static void main(String[] args) throws IOException { 
		  final HttpServer server = new WebApp().startServer();
		  System.out.println(String.format("Jersey app started with WADL available at "
	                + "%sapplication.wadl\nHit enter to stop it...", BASE_URI));
	        System.in.read();
	        server.shutdownNow();
		 } 
	
		 @Override
		 public HttpServer startServer() throws IOException { 
		 
		  // Create test web application context. 
		  final WebappContext ctx = new WebappContext("Test Context", "/rest"); 
		  
		  //https://github.com/janusdn/jersey2-spring4-grizzly2/blob/master/pom.xml
		  
		  ctx.addContextInitParameter("contextClass", "org.springframework.web.context.support.XmlWebApplicationContext");
		  //以下三行可以不配置,默认配置在classpath:applicationContext.xml下
		  ctx.addContextInitParameter("contextConfigLocation", "classpath*:spring-context.xml"); 
		  ctx.addListener("org.springframework.web.context.ContextLoaderListener"); 
		  ctx.addListener("org.springframework.web.context.request.RequestContextListener");
		 
		  //ctx.addFilter("auth",  serv.AuthorizationFilter.class);
		 
		  // Create a servlet registration for the web application in order to wire up Spring managed collaborators to Jersey resources. 
		  ServletRegistration servletRegistration = ctx.addServlet("jersey-servlet", ServletContainer.class); 
		 
		  // The logging filters for server logging. 
		  //servletRegistration.setInitParameter("com.sun.jersey.spi.container.ContainerResponseFilters", "com.sun.jersey.api.container.filter.LoggingFilter"); 
		  //servletRegistration.setInitParameter("com.sun.jersey.spi.container.ContainerRequestFilters", "com.sun.jersey.api.container.filter.LoggingFilter"); 
		  servletRegistration.setInitParameter("javax.ws.rs.Application", " serv.MyDemoApplication"); 		 
		  //servletRegistration.setInitParameter("com.sun.jersey.config.property.packages", " serv.rest"); 
		  
		  //servletRegistration.setInitParameter("com.sun.jersey.api.json.POJOMappingFeature", "true"); 
		  servletRegistration.addMapping("/v1/*"); 
		  
		 
		 // final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), false);
	        
		  HttpServer server = new HttpServer(); 
		  NetworkListener listener = new NetworkListener("grizzly2", "localhost", 80); 
		  server.addListener(listener); 
		 
		  ctx.deploy(server); 
		  server.start(); 
		  return server;
		   
	} 
}

加个rest服务接口类即可:

package  serv.rest;

import java.util.List;

import javax.annotation.security.DenyAll;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.springframework.beans.factory.annotation.Autowired;

import  serv.dao.UserDao;
import  serv.po.User;

@Path("users")
//注意这个DenyAll标签是为了动态注入filter用,像这种情况必加
@DenyAll
public class UserServ {
	@Autowired
	private UserDao userDao;

	@GET
	@Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
	public User getUser(@PathParam("id") int id) {
		return userDao.getUser(id);
	}
	
	@GET	
    @Produces(MediaType.APPLICATION_JSON)
	public List<User> getUsers() {
		return userDao.getUsers();
	}
}

相关的UserDao.java:

package serv.dao;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import dao.BaseDao;
import serv.po.User;

@Component
@Transactional
public class UserDao{
	//BaseDao为hibernate的工具类(略)
	@Autowired	
	private BaseDao baseDao;	
		
	public User getUser(int userId) {		
		return baseDao.getFirstByHql("from User where id = ?", userId);		
	}
	
	public List<User> getUsers() {
		return baseDao.getList("from User");
	}
}

User.java类如下:

serv.po;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "users")
public class User implements Serializable{
	private static final long serialVersionUID = -2392120550365139069L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
		
	@Column(name="uname", length=20)
	private String uname;	
	
	@Column(name="address", length=50)
	private String address;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getUname() {
		return uname;
	}
	public void setUname(String uname) {
		this.uname = uname;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
}

这时候启动WebApp,访问http://localhost/rest/user/1即可得到id为1的用户信息,通过以下junit测试类也可以模拟:

serv.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;

import org.glassfish.grizzly.http.server.HttpServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import utils.GsonUtil;

public class RestTest{
	private HttpServer server;
    private WebTarget target;
    
    @Before
    public void setUp() throws Exception {
        // start the server
        server = WebApp.class.newInstance().startServer();
        // create the client
        Client c = ClientBuilder.newClient();	

        // uncomment the following line if you want to enable
        // support for JSON in the client (you also have to uncomment
        // dependency on jersey-media-json module in pom.xml and Main.startServer())
        // --
        // c.configuration().enable(new org.glassfish.jersey.media.json.JsonJaxbFeature());

        target = c.target(IApp.BASE_URI);
    }

    //关闭http server
    @After
    public void tearDown() throws Exception {
        server.shutdownNow();
    }
    
    @Test
    public void testGetUser() {
        String responseMsg = target.path("/v1/user/1").request().get(String.class);        
        assertTrue(GsonUtil.isSameJsonStr("{\"id\":1,\"uname\":\"daniel\",\"address\":\"sz\"}", responseMsg));
    }
}

2.容器里的rest服务

web.xml内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>xltx_service</display-name>
  <!-- 加载spring文件 -->
  <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-context.xml</param-value>
    </context-param>
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
	
	<servlet>
		<servlet-name>jersey-servlet</servlet-name>
		<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
		<init-param>
			<param-name>javax.ws.rs.Application</param-name>
			<param-value>serv.ApplicationResConfigure</param-value>
		</init-param>
	</servlet>
	
	<servlet-mapping>
		<servlet-name>jersey-servlet</servlet-name>
		<url-pattern>/v1/*</url-pattern>
	</servlet-mapping>
</web-app>

启动tomcat即可以访问rest接口

3.统一的异常处理

自定义的几个异常类:

package serv.exception;

import javax.ws.rs.core.Response;

public class ExceptionBean {
	protected int code = 0000;
	protected String msg = "unkown message";
	protected Response.Status status = Response.Status.INTERNAL_SERVER_ERROR;
	public ExceptionBean() {
		
	}
	public ExceptionBean(Response.Status status, int code, String msg) {
		this.code = code;
		this.status = status;
		this.msg = msg;
	}
	
	public int getCode() {
		return code;
	}
	public void setCode(int code) {
		this.code = code;
	}
	public String getMsg() {
		return msg;
	}
	public void setMsg(String msg) {
		this.msg = msg;
	}
	public Response.Status getStatus() {
		return status;
	}
	public void setStatus(Response.Status status) {
		this.status = status;
	}
}

自定义异常类:

package serv.exception;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;


public class AuthException extends MyException {
	private static final long serialVersionUID = -6611852001930903768L;
		
	public AuthException() {
	}

	public AuthException(String msg) {
		this(0, msg);
	}
	
	public AuthException(int code, String msg, String... params) {
		this(Status.INTERNAL_SERVER_ERROR, code, msg, params);
	}
	
	public AuthException(Response.Status status, int code, String msg, String... params) {
		super(msg);
		this.status = status;
		this.code = code;
		this.msg = msg;
		
		if (null != params) {
//			values = new Object[params.length];
//			for (int i = 0; i < params.length; i++) {
//				values[i] = params[i];
//			}
			values = params;
		}
	}
	
	public AuthException(String msg, Throwable cause, String... params){
		this(0, msg, cause, params);
	}

	public AuthException(int code, String msg, Throwable cause, String... params) {
		this(Status.INTERNAL_SERVER_ERROR, code, msg, params);
	}
	
	public AuthException(Response.Status status, int code, String msg, Throwable cause, String... params) {
		super(msg, cause);
		this.status = status;
		this.code = code;
		this.msg = msg;
		
		if (null != params) {
			values = new Object[params.length];
			for (int i = 0; i < params.length; i++) {
				values[i] = params[i];
			}
			values = params;
		}
	}
}

自定义异常父类:

package serv.exception;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class MyException extends RuntimeException {

	private static final long serialVersionUID = -1322574745106779184L;
	protected Object[] values;
	protected int code = 0000;
	protected String msg = "unkown message";
	protected Response.Status status;
	
	public MyException() {
	}

	public MyException(String msg) {
		this(0, msg);
	}
	
	public MyException(int code, String msg, String... params) {
		this(Status.INTERNAL_SERVER_ERROR, code, msg, params);
	}
	
	public MyException(Response.Status status, int code, String msg, String... params) {
		super(msg);
		this.status = status;
		this.code = code;
		this.msg = msg;
		
		if (null != params) {
//			values = new Object[params.length];
//			for (int i = 0; i < params.length; i++) {
//				values[i] = params[i];
//			}
			values = params;
		}
	}
	
	public MyException(String msg, Throwable cause, String... params){
		this(0, msg, cause, params);
	}

	public MyException(int code, String msg, Throwable cause, String... params) {
		this(Status.INTERNAL_SERVER_ERROR, code, msg, params);
	}
	
	public MyException(Response.Status status, int code, String msg, Throwable cause, String... params) {
		super(msg, cause);
		this.status = status;
		this.code = code;
		this.msg = msg;
		
		if (null != params) {
			values = new Object[params.length];
			for (int i = 0; i < params.length; i++) {
				values[i] = params[i];
			}
			values = params;
		}
	}

	public Object[] getValues() {
		return values;
	}

	public void setValues(Object[] values) {
		this.values = values;
	}

	public int getCode() {
		return code;
	}

	public void setCode(int code) {
		this.code = code;
	}

	public Response.Status getStatus() {
		return status;
	}
	
	public ExceptionBean getResource() {
		return new ExceptionBean(this.status, this.code, this.msg);
	}
}

异常类统一来处理:

package serv.exception;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class MyExceptionMapper implements ExceptionMapper<Exception> {
    @Override
    public Response toResponse(Exception exception) {
    	ExceptionBean exResource = new ExceptionBean();
    			
    	if (exception instanceof MyException) {
    		exResource = ((MyException)exception).getResource();
    		
    	} else {
    		if (exception.getMessage() != null) {
    			exResource.setMsg(exception.getMessage());
    		}
    	}
    	return Response.ok(exResource).status(exResource.getStatus()).build();
    }
}

4.oauth2.0实现

oauth2.0实现一个简化版本,大概的流程如下:

st=>start: Start
op=>operation: Your Operation
cond=>condition: Yes or No?
e=>end
st->op->cond
cond(yes)->e
cond(no)->op
st=>start: start
op1=>operation: 使用appid获取code

op2=>operation: 使用
appid/code/secret
获取access_token
(2小时有效)
/refresh_token
(30天有效)

op3=>operation: 使用appid/access_token访问认证资源
op4=>operation: 返回认证资源
op5=>operation: 使用refresh_token刷新access_token

cond2=>condition: access_token有效?
cond3=>condition: refresh_token有效?
cond_code_ok=>condition: code有效?

e=>end: End

st->op1->op2->cond_code_ok->op3->cond2->op4->e
cond2(yes)->op4
cond2(no)->op5->cond3
cond3(no)->op1
cond_code_ok(yes)->op3
cond_code_ok(no)->op1

建表:

CREATE TABLE `auth` (
  `app_id` varchar(18) NOT NULL,
  `app_secret` varchar(32) NOT NULL,
  `access_token` varchar(88) NOT NULL,
  `refresh_token` varchar(88) NOT NULL,
  `access_token_expires_in` smallint(6) NOT NULL,
  `refresh_token_expires_ltm` bigint(20) NOT NULL, /*失效时间戳*/
  `access_token_expires_ltm` bigint(20) NOT NULL,
  PRIMARY KEY (`app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

PO对象类:

package serv.po;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "auth")
public class AuthInfo {
	@Id
	@Column(name="app_id", length=18)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	String appId;	

	@Column(name="app_secret", length=32)
	String appSecret;	

	@Column(name="access_token", length=88)
	String accessToken;	

	@Column(name="refresh_token", length=88)
	String refreshToken;
	
	@Column(name="access_token_expires_in")
	int expires_in;
	
	@Column(name="access_token_expires_ltm", length=20)
	long accessTokenExpiresLtm;
	
	@Column(name="refresh_token_expires_ltm", length=20)
	long refreshTokenExpiresLtm;		
	
	public String getAppId() {
		return appId;
	}
	public void setAppId(String appId) {
		this.appId = appId;
	}
	public String getAppSecret() {
		return appSecret;
	}
	public void setAppSecret(String appSecret) {
		this.appSecret = appSecret;
	}
	
	public String getAccessToken() {
		return accessToken;
	}
	public void setAccessToken(String accessToken) {
		this.accessToken = accessToken;
	}
	public String getRefreshToken() {
		return refreshToken;
	}
	public void setRefreshToken(String refreshToken) {
		this.refreshToken = refreshToken;
	}
	public int getExpires_in() {
		return expires_in;
	}
	public void setExpires_in(int expires_in) {
		this.expires_in = expires_in;
	}
	public long getAccessTokenExpiresLtm() {
		return accessTokenExpiresLtm;
	}
	public void setAccessTokenExpiresLtm(long accessTokenExpiresLtm) {
		this.accessTokenExpiresLtm = accessTokenExpiresLtm;
	}
	public long getRefreshTokenExpiresLtm() {
		return refreshTokenExpiresLtm;
	}
	public void setRefreshTokenExpiresLtm(long refreshTokenExpiresLtm) {
		this.refreshTokenExpiresLtm = refreshTokenExpiresLtm;
	}
}

接下来有三个类,AuthDao——处理数据表,AuthService——提供数据服务,AuthServ——提供rest接口服务

package serv.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import dao.BaseDao;
import serv.po.AuthInfo;

@Component
@Transactional
public class AuthDao {
	@Autowired
	private BaseDao baseDao;
		
	public AuthInfo getAuthInfo(String appId) {
		return baseDao.getFirstByHql("from AuthInfo where appId=?", appId);
	}
	
	public void saveAuthInfo(AuthInfo authInfo) {
		baseDao.saveOrUpdate(authInfo);
	}
}

package serv.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import dao.AuthDao;
import po.AuthInfo;

@Service
public class AuthService {
	@Autowired
	private AuthDao authDao;
	
	public AuthInfo getAuthInfo(String appId) {
		return authDao.getAuthInfo(appId);
	}
	
	public void saveAuthInfo(AuthInfo authInfo) {
		authDao.saveAuthInfo(authInfo);
	}
}
package serv.rest;

import javax.annotation.security.PermitAll;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import org.springframework.beans.factory.annotation.Autowired;

import utils.StringUtils;
import serv.exception.AuthException;
import serv.po.AuthInfo;
import serv.service.AuthService;
import serv.tools.CommConst;
import serv.tools.FluzzyMap;

//outl https://github.com/zhangkaitao/shiro-example/blob/master/shiro-example-chapter17-server/src/main/java/com/github/zhangkaitao/shiro/chapter17/web/controller/AccessTokenController.java
@Path("oauth2")
//注意标签PermitAll,用于动态加filter,像这种情况不加
@PermitAll
public class AuthServ {
	@Autowired
	private AuthService authService;
	
	//根据appid获得code,code可以有多个,不放在数据库中
	@GET
	@Path("/auth")
	@Produces(MediaType.APPLICATION_JSON)
	public String auth(@QueryParam("appid") String appId){
		if (authService.getAuthInfo(appId) == null) {
			return "{\"errmsg\":\"appid required or is error\"}";
		}
		//返回新code TODO 从缓存拿
		String code = StringUtils.getID(CommConst.LEN_CODE);//"0612iyQs0dBnqe13vkPs0zaNQs02iyQz";
		FluzzyMap.putData(appId, code);
		
		return new String("{\"code\":\""+ code + "\"}");//len 32
	}
	
	@GET
	@Path("/access_token")
	@Produces(MediaType.APPLICATION_JSON)
	public String getAccessToken(@QueryParam("appid") String appId, 
								 @QueryParam("secret") String secret, 
								 @QueryParam("code") String code){
		//len 88
		//判断code是否有效
		String validCode = FluzzyMap.verifyData(appId, code);
		
		if (StringUtils.isEmpty(validCode)) {
			return "{\"errcode\":4001, \"errmsg\":\"invalid code or expired\"}";
		}
		//查看appid是否有效
		AuthInfo auInfo = authService.getAuthInfo(appId);
		if (auInfo == null) {
			throw new AuthException("invalid appid");
		}
		//生成access_token/refresh_token并返回
		String access_token = StringUtils.getID(CommConst.LEN_ACCESS_TOKEN);
		String refresh_token = StringUtils.getID(CommConst.LEN_REFRESH_TOKEN);
		long current = System.currentTimeMillis() / 1000;
		auInfo.setAccessToken(access_token);
		auInfo.setRefreshToken(refresh_token);
		auInfo.setAccessTokenExpiresLtm(current + 7200);
		auInfo.setRefreshTokenExpiresLtm(current + 30*24*3600);
		authService.saveAuthInfo(auInfo);
		//base对应 /user/*,/address/*等,后续可能有后台相关等
		return new String("{\"access_token\":\""+access_token+"\", \"expires_in\":7200,\"refresh_token\":\""+refresh_token+"\",\"scope\":\"snsapi_base\",\"app_id\":\""+appId+"\"}");
	}
	
	@GET	
	@Path("/refresh_token")
	@Produces(MediaType.APPLICATION_JSON)
	public String refreshAccessToken(@QueryParam("appid") String appid, 
			                         @QueryParam("refresh_token") String refresh_token){
		//查看appid是否有效
		AuthInfo auInfo = authService.getAuthInfo(appid);
		
		if (auInfo == null) {			
			return "{\"errcode\":4000, \"errmsg\":\"invalid appid\"}";
		}
		long current = System.currentTimeMillis() / 1000;
		
		if (!refresh_token.equals(auInfo.getRefreshToken()) 
				|| current > auInfo.getRefreshTokenExpiresLtm()){
			throw new AuthException("refresh token is not latest or is expires");
		}
		auInfo.setAccessTokenExpiresLtm(current + 7200);
		authService.saveAuthInfo(auInfo);
		
		//base对应 /user/*,/address/*等,后续可能有后台相关等
		return new String("{\"access_token\":\""+auInfo.getAccessToken()+"\", \"expires_in\":7200,\"refresh_token\":\""+auInfo.getRefreshToken()+"\",\"scope\":\"snsapi_base\",\"appid\":\""+appid+"\"}");
	}
}

引用的其它几类,常量类:

package serv.tools;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class CommConst {
	public static final int LEN_ACCESS_TOKEN = 88;
	public static final int LEN_REFRESH_TOKEN = 88;
	public static final int LEN_CODE = 32;
	public static final int LEN_APPID = 18;
	public static final int LEN_SECRET = 32;
	public static Response.Status STATUS_UNAUTHORIZED = Response.Status.UNAUTHORIZED;
}

缓存code的类:

package serv.tools;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class FluzzyMap {
	private static Map<String, Set<String>> fluzzyMap = new HashMap<String, Set<String>>();
	
	public static void putData(String k, String v) {
		Set<String> lv = fluzzyMap.get(k);
		
		if (lv == null) {
			lv = new HashSet<String>();
			fluzzyMap.put(k, lv);
		}
		lv.add(v);
	}
	//get and remove
	public static String verifyData(String k, String v) {
		if (fluzzyMap.containsKey(k)) {
			Set<String> lv = fluzzyMap.get(k);
			if (lv != null && !lv.isEmpty()) {
				if (!lv.contains(v)) {
					return null;
				} else {
					lv.remove(v);
					return v;
				}
			}
		}
		return null;
	}
}

有两类情况,如果请求身份验证的相关接口,不应该要求access_token,否则要,见前文的标签@PermitAll、@DenyAll,首先需要一个filter:

package serv;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import javax.annotation.Priority;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.XmlWebApplicationContext;

import corelib.utils.StringUtils;
import serv.exception.AuthException;
import serv.po.AuthInfo;
import serv.service.AuthService;
import serv.tools.Code;
import serv.tools.CommConst;

//http://www.jianshu.com/p/a1c2b6c16118
//rest拦截器
public class AuthFilter implements ContainerRequestFilter {
	private AuthService authService;
	private final List<String> rolesAllowed;

	//通过feature实现类(能autowired)进行构造
	public AuthFilter(AuthService authService) {
		this(authService, null);
	}
	
	public AuthFilter(final String... rolesAllowed) {
        this(null, Arrays.asList(rolesAllowed));
    }

    public AuthFilter(AuthService authService, final List<String> rolesAllowed) {
    	this.authService = authService;
        this.rolesAllowed = rolesAllowed;
    }
    
	
	@Override
	public void filter(ContainerRequestContext reqContext) throws IOException {
		//https://github.com/mestevens/dynamic-role-filter/blob/master/src/main/java/ca/mestevens/java/server/filter/DynamicRoleFilter.java
		/*
		final MultivaluedMap<String, String> propertyNames = reqContext.getUriInfo().getPathParameters();
		final SecurityContext securityContext = reqContext.getSecurityContext();
		
		final boolean authorized = securityContext.getUserPrincipal() != null 
				 && rolesAllowed.stream()
	                        .map(role -> propertyNames.keySet().stream()
	                                .filter(role::contains)
	                                .reduce(role, (r1, r2) -> r1.replace(String.format("{%s}", r2), propertyNames.getFirst(r2))))
	                        .anyMatch(securityContext::isUserInRole);
		*/
		final MultivaluedMap<String, String> params = reqContext.getUriInfo().getQueryParameters();
		//获取客户端Header中提交的token
		String access_token = params.getFirst("access_token");
		String app_id = params.getFirst("appid");
        verify(app_id, access_token);
	}
	
	//https://github.com/mestevens/dynamic-role-filter/blob/master/src/main/java/ca/mestevens/java/server/filter/DynamicRoleFilter.java
	//验证token的有效性
	private void verify(String app_id, String access_token) {
		if (StringUtils.isEmpty(app_id) || StringUtils.isEmpty(access_token)) {
			throw new AuthException(CommConst.STATUS_UNAUTHORIZED, Code.NO_AUTH, "appid and access_token required");
		} 
		//无法注入,也无法通过context做文章,痛苦地找解决方案
 		AuthInfo authInfo = authService.getAuthInfo(app_id);
 		
		if (authInfo == null) {
			throw new AuthException(CommConst.STATUS_UNAUTHORIZED, Code.NO_AUTH, "appid is invalid");
		}
		long current = System.currentTimeMillis() / 1000;
		
		if (current > authInfo.getAccessTokenExpiresLtm()) {
			throw new AuthException(CommConst.STATUS_UNAUTHORIZED, Code.NO_AUTH, "access_token is expired");
		}
	}	
}

然后还需要动态加载:

package serv;

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.container.DynamicFeature;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.FeatureContext;

import org.glassfish.jersey.server.model.AnnotatedMethod;
//https://github.com/mestevens/dynamic-role-filter/blob/master/src/main/java/ca/mestevens/java/server/filter/DynamicRolesDynamicFeature.java
//https://github.com/mestevens/dynamic-role-filter/blob/master/src/test/java/ca/mestevens/java/dropwizard/rest/TestResource.java
import org.springframework.beans.factory.annotation.Autowired;

import serv.service.AuthService;

public class DynamicRolesDynamicFeature implements DynamicFeature {

	@Autowired
	private AuthService authService;
	
	@Override
	public void configure(ResourceInfo resourceInfo, FeatureContext featureContext) {
		final AnnotatedMethod method = new AnnotatedMethod(resourceInfo.getResourceMethod());
		
		// 方法加DenyAll标签,必走filter
		if (method.isAnnotationPresent(DenyAll.class)) {
			featureContext.register(new AuthFilter(authService));
			return;
		}

		final RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
		// 需要判断权限,根据标签@RolesAllowed("user:{user}:read")定义权限
		if (rolesAllowed != null) {
			featureContext.register(new AuthFilter(rolesAllowed.value()));
			return;
		}

		if (method.isAnnotationPresent(PermitAll.class)) {
			return;
		}

		// 类?
		final Class<?> resourceClass = resourceInfo.getResourceClass();
		if (resourceClass.isAnnotationPresent(DenyAll.class)) {
			//这也行 https://github.com/alanAraujoSousa/project/blob/06872886d2c08f2b374a36a251ab31205e0cd71f/server/engine/src/main/java/br/com/engine/controller/rest/context/AuthorizationFeature.java
			featureContext.register(new AuthFilter(authService));
			return;
		}

		final RolesAllowed classRolesAllowed = resourceClass.getAnnotation(RolesAllowed.class);
		
		if (classRolesAllowed != null) {
			featureContext.register(new AuthFilter(classRolesAllowed.value()));
		}

	}

}

loading