JAX-RS,全称为Java API for RESTful Web Services,具体的指支持REST架构风格创建Web服务的规范,具体的实现有Apache CXF、Jersey、RESTEasy等,本文是由的是apache的实现cxf。
1、如何使用springboot发布jax-rs服务。
2、如何在jax-rs服务中配置拦截器并实现基于token的权限校验功能。
让我们开始把~
老规矩贴一下pom文件
<?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: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.syx</groupId>
<artifactId>cxf-springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<cxf.version>3.2.4</cxf.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxrs</artifactId>
<version>3.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-jetty</artifactId>
<version>3.2.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
1、定义一个用于资源访问的接口(直接使用类也行),同时使用jax-rs的注解进行标注,@PATH代表资源的请求路径,@Produces代表该接口响应的数据类型。在接口声明的方法上,@POST代表以POST类型的请求方式,@Consumes代表该方法所接受的参数类型。同时我们可以和客户端定义好接口方法中参数的json类型。
package com.syx.endpoint;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
@Path("/syxEndpoint")
@Produces({"application/json"})
public interface SYXEndPoint {
/**
*
* @param args {
* "RuleName":"",
* "Auth": "",
* "RequestParam": {
*
* }
* }
* @return
*/
@POST
@Path("/service")
@Consumes({"application/json"})
String service(String args);
}
2、定义上面声明的资源接口的实现类,由于我们需要使用spring的ioc功能,所以标注上@Compoent注解,将其交给ioc容器管理。
package com.syx.endpoint.impl;
import com.alibaba.fastjson.JSON;
import com.syx.endpoint.SYXEndPoint;
import com.syx.endpoint.handler.Handler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Map;
@Component
public class SYXEndPointImpl implements SYXEndPoint {
@Autowired
private ApplicationContext ctx;
@Override
public String service(String args) {
Map<String,Object> map = JSON.parseObject(args, Map.class);
String ruleName = (String) map.get("RuleName");
Handler handler = (Handler) ctx.getBean(ruleName);
Map<String,Object> requestParam = (Map<String, Object>) map.get("RequestParam");
return handler.service(requestParam).toJsonString();
}
}
3、配置jax-rs服务并发布(由于我们使用的是cxf-rt-transports-http-jetty这个依赖,cxf将使用jetty服务器发布webService服务,当然还有其他的实现,比如netty、tomcat)
package com.syx.config;
import com.syx.endpoint.SYXEndPoint;
import com.syx.endpoint.interceptor.AuthInterceptor;
import org.apache.cxf.bus.spring.SpringBus;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import javax.annotation.PostConstruct;
import java.util.Collections;
@Configuration
@ImportResource(value = {"classpath:/META-INF/cxf/cxf.xml"})//加载类路径下的cxf配置文件,用于初始化SpringBus
public class EndpointConfig {
@Autowired
private SpringBus bus;
@Autowired
private SYXEndPoint syxEndPoint;
@PostConstruct
public void init(){
JAXRSServerFactoryBean serverFactoryBean = new JAXRSServerFactoryBean();
//配置服务实现类
serverFactoryBean.setServiceBean(syxEndPoint);
//配置服务的发布地址
serverFactoryBean.setAddress("http://127.0.0.1:9999/cxf/");
//添加请求响应对应的输入输出日志拦截器
serverFactoryBean.setInInterceptors(Collections.singletonList(new LoggingInInterceptor()));
serverFactoryBean.setOutInterceptors(Collections.singletonList(new LoggingOutInterceptor()));
//添加自定义权限控制拦截器
serverFactoryBean.getInInterceptors().add(new AuthInterceptor());
serverFactoryBean.create();
}
}
4、自定义权限拦截器并实现基于token的权限校验功能,这里我们自定一个权限校验类然后继承AbstractPhaseInterceptor同时重写handleMessage和handleFault两个方法,其中handleMessage主要是用于在请求我们的业务方法前进行一个拦截,这里我们配置了一个简单的拦截规则,如果用户请求的是获取token的处理器我们就直接放行,如果是其他的处理器我们的程序就进行token的校验,还有一部分权限资源的控制我没实现,大家可以自行实现(其实就是业务handler和角色的匹配)。handleFault方法主要是用于捕获我们在handleMessage处理中的异常,譬如token过期、token校验不通过等。目前这个自定义的权限拦截器比较粗糙(目前只处理Exception类型的错误,其实这个异常可以更准确 ),大家可以在handleFault中添加更多的异常处理类型。
package com.syx.endpoint.interceptor;
import com.alibaba.fastjson.JSON;
import com.syx.auth.jwt.JwtUtil;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Message;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.transport.http.AbstractHTTPDestination;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* 用于token身份令牌校验
*/
public class AuthInterceptor extends AbstractPhaseInterceptor<Message> {
public AuthInterceptor() {
super(Phase.PRE_INVOKE);
}
@Override
public void handleMessage(Message message) throws Fault {
List content = message.getContent(List.class);
String o = (String) content.get(0);
Map map = JSON.parseObject(o, Map.class);
String ruleName = (String) map.get("RuleName");
if("TokenHandler".equals(ruleName)){
return;
}
String token = (String) map.get("Auth");
//token合法性校验
JwtUtil.isExpiration(token);
//解析token中的角色,进行资源使用的权限校验(自行实现。。。)
}
@Override
public void handleFault(Message message) {
Exception ex = message.getContent(Exception.class);
HttpServletResponse resp = (HttpServletResponse)message.getExchange()
.getInMessage().get(AbstractHTTPDestination.HTTP_RESPONSE);
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
resp.setContentType("text/plain");
try {
resp.getOutputStream().write(ex.getMessage().getBytes());
resp.getOutputStream().flush();
message.getInterceptorChain().setFaultObserver(null); //avoid return soap fault
message.getInterceptorChain().abort();
} catch (IOException e) {
// TODO
}
}
}
1、JwtToken的工具类
package com.syx.auth.jwt;
import com.syx.entity.UserVo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.List;
public class JwtUtil {
// 主题
private static final String SUBJECT = "zx";
// jwt的token有效期,
private static final long EXPIRITION = 1000L * 60 * 60 * 24 * 7;//7天
// private static final long EXPIRITION = 1000L * 60 * 30; // 半小时
// 加密key(黑客没有该值无法篡改token内容)
private static final String APPSECRET_KEY = "zxdc";
// 用户url权限列表key
private static final String AUTH_CLAIMS = "auth";
/**
* TODO 生成token
*
* @param user
* @return java.lang.String
* @date 2020/7/6 0006 9:26
*/
public static String generateToken(UserVo user) {
String token = Jwts
.builder()
// 主题
.setSubject(SUBJECT)
// 添加jwt自定义值
.claim(AUTH_CLAIMS, user.getRole())
.claim("username", user.getUserName())
.claim("userId", user.getUid())
.setIssuedAt(new Date())
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
// 加密方式,加密key
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
return token;
}
/**
* 获取用户Id
*
* @param token
* @return
*/
public static String getUserId(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("userId").toString();
}
/**
* 获取用户名
*
* @param token
* @return
*/
public static String getUsername(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("username").toString();
}
/**
* 获取用户角色列表, 没有返回空
*
* @param token
* @return
*/
public static List<String> getUserAuth(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return (List<String>) claims.get(AUTH_CLAIMS);
}
/**
* 验证是否过期
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
System.out.println("过期时间: " + claims.getExpiration());
return claims.getExpiration().before(new Date());
}
}
2、DataSourceConfig数据源配置类
package com.syx.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.sql.SQLException;
@Configuration
@ConditionalOnClass(DataSource.class)
@EnableTransactionManagement
public class DataSourceConfig {
@Bean
@Primary
public DataSource ds() throws SQLException {
DruidDataSource ds = new DruidDataSource();
ds.setUrl("jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true");
ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds.setUsername("root");
ds.setPassword("whh123");
ds.setInitialSize(3);
ds.setMinIdle(3);
ds.setMaxActive(5);
ds.setTestWhileIdle(true);
ds.setValidationQuery("select 1");
ds.init();
return ds;
}
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory(DataSource ds) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(ds);
//配置mybatis的mapper.xml的路径,由于我们使用的是mybatis注解,所以注掉了
// bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResource("classpath:database/**/*.xml"));
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setUseGeneratedKeys(true);
configuration.setCacheEnabled(true);
configuration.setLazyLoadingEnabled(false);
configuration.setLogPrefix("dao.");
//添加分页插件
configuration.addInterceptor(new PageInterceptor());
configuration.setJdbcTypeForNull(JdbcType.NULL);
bean.setConfiguration(configuration);
return bean.getObject();
}
}
3、CompanyDao
package com.syx.dao;
import com.syx.entity.Company;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface CompanyDao {
@Select("select * from company")
List<Company> findAll();
}
4、业务处理的handler,我都粘贴到一起了,大家自行拆开
package com.syx.endpoint.handler;
import com.syx.response.CommonResponse;
import java.util.Map;
public interface Handler {
CommonResponse service(Map<String,Object> args);
}
//-----------------------------------------------------------
package com.syx.endpoint.handler;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.syx.dao.CompanyDao;
import com.syx.entity.Company;
import com.syx.response.CommonResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component("CompanyHandler")
public class CompanyHandler implements Handler {
@Autowired
private CompanyDao companyDao;
@Override
public CommonResponse service(Map<String,Object> args) {
PageHelper.startPage(1,2);
List<Company> all = companyDao.findAll();
PageInfo<Company> pageInfo = new PageInfo<>(all);
return CommonResponse.success(pageInfo,null);
}
}
//-----------------------------------------------------------
package com.syx.endpoint.handler;
import com.syx.auth.jwt.JwtUtil;
import com.syx.entity.UserVo;
import com.syx.response.CommonResponse;
import com.syx.response.CommonResponseCode;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@Component("TokenHandler")
public class TokenHandler implements Handler {
Map<Integer, UserVo> userMap = new HashMap<Integer, UserVo>(){
{
UserVo u1 = new UserVo();
u1.setUid(1);
u1.setRole(Arrays.asList("1","2"));
u1.setUserName("张三");
put(u1.getUid(),u1);
}
};
/**
* {
* "RuleName":"TokenHandler",
* "Auth": "",
* "RequestParam": {
* "UserId":"123",
* "Username":"张三"
* }
* }
* @param args
* @return
*/
@Override
public CommonResponse service(Map<String, Object> args) {
int userId = Integer.parseInt((String) args.get("UserId"));
String userName = (String) args.get("Username");
//数据库查询是否有该用户,如果有,生成token,如果没有返回无权访问
UserVo userVo = userMap.get(userId);
if(userVo == null){
return CommonResponse.fail(CommonResponseCode.RC400,"用户"+userId+"不存在");
}
return CommonResponse.success( JwtUtil.generateToken(userVo),null);
}
}
5、实体类
package com.syx.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class Company {
private int id;
private String name;
private int age;
private String address;
private double salary;
}
//--------------------------------------------
package com.syx.entity;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Setter
@Getter
public class UserVo {
/**
* 账户id
*/
private int uid;
/**
* 姓名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 是否可用 1可用 0不可用
*/
private Integer lock;
/**
* 角色列表
*/
private List<String> role;
}
6、统一响应的包装类
package com.syx.response;
import com.alibaba.fastjson.JSON;
import org.springframework.util.StringUtils;
public class CommonResponse<T> {
private int status;
private String msg;
private T data;
private long timeStamp;
private CommonResponse() {
this.timeStamp = System.currentTimeMillis();
}
/**
* 成功
* @param data
* @param msg
* @param <T>
* @return
*/
public static <T> CommonResponse success(T data,String msg){
CommonResponse response = new CommonResponse<>();
response.status = CommonResponseCode.RC200.getCode();
if(StringUtils.isEmpty(msg)){
response.msg = CommonResponseCode.RC200.getMsg();
}else{
response.msg = msg;
}
response.data = data;
return response;
}
public static <T> CommonResponse<T> fail(CommonResponseCode code,String msg){
CommonResponse response = new CommonResponse();
response.status = code.getCode();
if(StringUtils.isEmpty(msg)){
response.msg = code.getMsg();
}else{
response.msg = msg;
}
return response;
}
/**
* 转换成json
* @return
*/
public String toJsonString(){
return JSON.toJSONString(this);
}
public int getStatus() {
return status;
}
public String getMsg() {
return msg;
}
public T getData() {
return data;
}
public long getTimeStamp() {
return timeStamp;
}
}
//----------------------------------------
package com.syx.response;
public enum CommonResponseCode {
RC200(200,"SUCCESS"),
RC400(400,"FAIL"),
RC500(500,"Internal ERROR")
;
private final int code;
private final String msg;
CommonResponseCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
7、springboot的启动类
package com.syx;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = "com.syx.**.dao")
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class);
}
}