陋室铭


  • 首页

  • 分类

  • 归档

  • 标签
陋室铭

App项目之服务高级篇

发表于 2016-12-25 | 分类于 Java | 阅读次数

写在前面

  • App项目之服务基础篇
  • App项目之服务进阶篇

我们在前两篇文章的基础上,继续探讨APP服务端的高级配置。那么,我们在此主要探讨的内容有:

  • 什么是REST
  • 传统的身份认证方法
  • 基于Token的身份认证
  • Token的生成之JWT
  • 程序示例

什么是REST

REST(Representational State Transfer)是一种软件架构风格。它将服务端的信息和功能等所有事物统称为资源,客户端的请求实际就是对资源进行操作。
它的主要特点有: – 每一个资源都会对应一个独一无二的url – 客户端通过HTTP的GET、POST、PUT、DELETE请求方法对资源进行查询、创建、修改、删除操作 – 客户端与服务端的交互必须是无状态的。

什么是RESTful架构:

  • 每一个URI代表一种资源;
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

传统的身份认证方法

Http是一种没有状态的协议,也就是服务端不知道谁是访问的应用。把用户看成是客户端,客户端使用用户名和密码进行身份认证,
不过这个客户端再次发送请求,还要验证。解决的方法是,当用户请求登录的时候,如果登录成功,那么在服务端生成一条记录
这条记录用于说明登录用户是谁,然后把这条记录的ID号发给客户端,客户端收到以后把这个ID存储到cookie中,下次这个用户
再向服务器发送请求的时候,可以带着这个Cookie,这样服务端验证一下这个Cookie里的信息,看看能不能在服务端找到对应的纪录,
如果可以,说明用户已经通过了身份验证,把用户请求的数据返回给客户端。

上面说的就是Session,服务端存储为登录的用户生成的Session,这些Session可能存储在内存,磁盘或者数据库中,并且需要服务端定期的清理过期的Session。

基于Token的身份认证

使用基于Token的身份认证,大概的流程是:

  1. 客户端使用用户名和密码请求登录
  2. 服务端接收请求,去验证用户名与密码
  3. 验证成功,服务端签发一个Token(有效期较短)和一个RefreshToken(有效期较长),再把Token和RefreshToken发送给客户端
  4. 客户端接收到Token和RefreshToken后,再把它们存储起来
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的Token
  6. 服务端接收到请求,然后去验证客户端请求里面的Token
  7. 如果验证成功,则向客户端返回请求结果;如果验证不成功,则用RefreshToken向服务器请求新的Token
  8. 服务端接收到请求,然后去验证RefreshToken
  9. 如果验证成功,则签发一个新的Token,把Token发给客户端;如果验证不成功,则告诉客户端refreshToken失效,重新登录

Token的生成之JWT

Token的生成方式有很多种,比较热门的有JWT(JSON WEB TOKEN),OAuth等。那我们采用JWT来生成我们的Token。
JWT 标准的 Token 有三个部分:

  • header
  • payload
  • signature

中间用点分隔开,并且都会使用Base64编码,其形式为:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Header

header 部分主要是两部分内容,一个是Token的类型,另一个是使用的算法,形式如下:

1
2
3
4
{
"typ": "JWT",
"alg": "HS256"
}

上面的内容用Base64的形式编码一下,变成:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload

Payload 里面是Token的具体内容,这些内容里面有一些事标准字段,同时可以添加自己需要的内容,标准字段如下:

  • iss: Issuer,发行者
  • sub: Subject,主题
  • aub: Audience,观众
  • exp,Expiration time,过期时间
  • nbf: Not before
  • iat: Issued at,发行时间
  • jti: JWT id

    比如下面的Payload,用到了sub,另外有两个自定义的字段,一个是name,还有一个是admin。

    1
    2
    3
    4
    5
    {
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
    }

    使用Base64编码以后变成这个样子:

    1
    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

    Signature

    JWT 的最后一部分是Signature,这部分内容有三个部分,先是用Base64编码的header.payload,再用加密算法加密一下,加密的时候
    要放进去一个Secret,这个相当于一个密码,这个密码秘密地存储在服务端。

    • header
    • payload
    • secret
    1
    HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload), 'secret')

处理完成以后看起来像这样:

1
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

客户端收到这个 Token 以后把它存储下来,下回向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源。

程序示例

  • JWT的生成
    JWT的生成,我们使用的是一个Java的开源库jjwt,添加引用
1
compile 'io.jsonwebtoken:jjwt:0.7.0'

同时使用该库,创建一个工具类,用于对token创建,检验等操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Component
public class JwtUtil {

@Value("${sign.key}")
private String secrect_key;

@Value("${spring.profiles.active}")
private String profiles;
private final Logger logger = LoggerFactory.getLogger(this.getClass());

/**
* 生成签名的Key
* @return
*/
private SecretKey generalKey(){
String stringKey=profiles+secrect_key;
byte[] encodedKey= Base64.decodeBase64(stringKey);
return new SecretKeySpec(encodedKey,0,encodedKey.length,"AES");
}

/**
* 生成JWT
* @param id
* @param subject
* @return
*/
public String createJWT(String id,String subject,long ttlMillis) throws Exception{
String token=null;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder=Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256,generalKey());
if(ttlMillis>0){
long expMillis = nowMillis + ttlMillis;
Date exp=new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}

/**
* 检验Token是否
* @param compactJws
* @return
*/
public boolean checkJWT(String compactJws){
boolean result=false;
try{
Claims claims=Jwts.parser().setSigningKey(generalKey()).parseClaimsJws(compactJws).getBody();
long expTime=claims.getExpiration().getTime();
long nowTime=System.currentTimeMillis();
if(expTime>nowTime){
result=true;
}
}catch (Exception e){
result=false;
logger.error("parseJWT",e);
}
return result;
}

/**
* 解析JWT字符串
* @param compactJws
* @return
*/
public Claims parseJWT(String compactJws){
Claims claims =null;
try {
claims = Jwts.parser().setSigningKey(generalKey()).parseClaimsJws(compactJws).getBody();
}catch (Exception e){
logger.error("checkJWT",e);
}
return claims;
}
}

创建一个TokenModel的类,存储于JWT的Subject

1
2
3
4
5
public class TokenModel {
private String userId;
private String roleId;
//getter和setter省略
}

Token的创建与检测的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
@Service
public class TokenServiceImpl implements TokenService{

@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private JwtUtil jwtUtil;

private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public String createAccessToken(String userId) {
String access_token=null;
TokenModel tokenModel=new TokenModel(userId);
try {
//生成Token
access_token = jwtUtil.createJWT(Constant.JWT_ID, JSONUtils.toJson(tokenModel), Constant.JWT_TTL);
//保存登录状态(存储于redis服务器中)
redisTemplate.boundValueOps(RedisUtils.generateTokenKey(userId)).set(access_token,Constant.JWT_TTL, TimeUnit.MILLISECONDS);
}catch (Exception e){
logger.error("---TokenServiceImpl---createToken---",e);
}
return access_token;
}

@Override
public String createRefreshToken(String userId) {
String refreshToken=null;
TokenModel tokenModel=new TokenModel(userId);
try {
//生成Token
refreshToken = jwtUtil.createJWT(Constant.JWT_ID, JSONUtils.toJson(tokenModel), Constant.JWT_REFRESH_TTL);
//保存登录状态
redisTemplate.boundValueOps(RedisUtils.generateRefreshTokenKey(userId)).set(refreshToken,Constant.JWT_REFRESH_TTL, TimeUnit.MILLISECONDS);
}catch (Exception e){
logger.error("---TokenServiceImpl---createToken---",e);
}
return refreshToken;
}

@Override
public boolean checkToken(String token) {
if(token==null){
return false;
}
Claims claims = jwtUtil.parseJWT(token);//解析获取token中的userId
if(claims!=null){
long expireTime=claims.getExpiration().getTime();
long currentTime=System.currentTimeMillis();
if(currentTime<expireTime){//表示已过期
TokenModel tokenModel=JSONUtils.fromJson(claims.getSubject(),TokenModel.class);
String userId=tokenModel.getUserId();
String redisToken=redisTemplate.opsForValue().get(RedisUtils.generateTokenKey(userId));
if(TextUtils.equals(token,redisToken)){//相同
//如果验证成功,说明此用户进行了一次有效操作,延长token的过期时间
//redisTemplate.boundValueOps(RedisUtils.generateTokenKey(userId)).expire(Constant.JWT_TTL, TimeUnit.MILLISECONDS);
return true;
}
}
}
return false;
}

@Override
public boolean checkRefreshToken(String refresh_token) {
if(refresh_token==null){
return false;
}
Claims claims = jwtUtil.parseJWT(refresh_token);//解析获取token中的userId
if(claims!=null){
long expireTime=claims.getExpiration().getTime();
long currentTime=System.currentTimeMillis();
if(currentTime<expireTime) {//表示已过期
TokenModel tokenModel=JSONUtils.fromJson(claims.getSubject(),TokenModel.class);
String userId=tokenModel.getUserId();
String redisToken=redisTemplate.opsForValue().get(RedisUtils.generateRefreshTokenKey(userId));
if(TextUtils.equals(refresh_token,redisToken)) {//相同
return true;
}
}
}
return false;
}

@Override
public void deleteToken(String userId) {
redisTemplate.delete(RedisUtils.generateTokenKey(userId));
}

@Override
public void deleteRefreshToken(String userId) {
redisTemplate.delete(RedisUtils.generateRefreshTokenKey(userId));
}

@Override
public TokenModel parseToken(String token) {
TokenModel tokenModel=null;
Claims claims = jwtUtil.parseJWT(token);//解析获取token中的userId
if(claims!=null){
tokenModel=JSONUtils.fromJson(claims.getSubject(),TokenModel.class);
}
return tokenModel;
}
}

再来看我们的TokenController类了:用户登录获取token以及通过refreshToken获取token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@RestController
@RequestMapping("/api/v1/")
public class TokenController {

@Autowired
private UserService userService;

@Autowired
private TokenService tokenService;

@ApiOperation(value="用户登录", notes="该API用于用户登录,成功后返回用户信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "用户登录账号", required = true, dataType = "String"),
@ApiImplicitParam(name = "password", value = "用户登录密码", required = true, dataType = "String")
})
@RequestMapping(value="login",method = RequestMethod.POST)
public ResponseEntity login(@RequestParam String username,@RequestParam String password){
Assert.notNull(username, "username can not be empty");
Assert.notNull(password, "password can not be empty");
UserModel userModel=userService.fetchUserByUserName(username);
if(userModel==null || !userModel.getPassword().equals(password)){
return ResponseUtil.custom("用户名或密码错误", Constant.USERNAME_OR_PASSWORD_ERROR);
}

//生成一个token,保存用户登录状态
String accessToken=tokenService.createAccessToken(userModel.getUserId());
String refreshToken=tokenService.createRefreshToken(userModel.getUserId());
if(accessToken==null||refreshToken==null){
return ResponseUtil.exception("创建token失败");
}
LoginResult loginResult=createResult(userModel);
loginResult.setAccess_token(accessToken);
loginResult.setExpire_in(Constant.JWT_TTL);
loginResult.setRefresh_token(refreshToken);
loginResult.setExpire_refresh_in(Constant.JWT_REFRESH_TTL);
return ResponseUtil.success("登录成功",loginResult);
}


@ApiOperation(value="获取token", notes="该API用于用户使用refreshToken获取token")
@ApiImplicitParams({
@ApiImplicitParam(name = "grant_type", value = "授权类型", required = true, dataType = "String"),
@ApiImplicitParam(name = "refresh_token", value = "refresh_token", required = true, dataType = "String")
})
@RequestMapping(value = "token",method = RequestMethod.POST)
public ResponseEntity refresh(@RequestParam String grant_type,@RequestParam String refresh_token){
Assert.notNull(grant_type, "username can not be empty");
Assert.notNull(refresh_token, "password can not be empty");

if(!TextUtils.equals(grant_type,"refresh_token")){
return ResponseUtil.paramError();
}
if(!tokenService.checkRefreshToken(refresh_token)){//验证未通过
return ResponseUtil.custom(Constant.RESCODE_EXPIRE_OR_NOTEXIST,"refresh_token不正确或者已过期");
}

TokenModel tokenModel=tokenService.parseToken(refresh_token);
//生成一个token,保存用户登录状态
String accessToken=tokenService.createAccessToken(tokenModel.getUserId());
if(accessToken==null){
return ResponseUtil.exception("创建token失败");
}
RefreshResult refreshResult=new RefreshResult();
refreshResult.setAccess_token(accessToken);
refreshResult.setRefresh_token(refresh_token);
return ResponseUtil.success("获取token成功",refreshResult);
}
/**
* 创建返回结果
* @param userModel
* @return
*/
private LoginResult createResult(UserModel userModel){
LoginResult loginResult=new LoginResult();
loginResult.setUserId(userModel.getUserId());
loginResult.setGender(userModel.getGender());
loginResult.setMobile(userModel.getMobile());
loginResult.setEmail(userModel.getEmail());
loginResult.setNickName(userModel.getNickName());
loginResult.setAvatar(userModel.getAvatar());
loginResult.setBio(userModel.getBio());
loginResult.setBlog(userModel.getBlog());
return loginResult;
}
}

此外,我们创建一个注解Authorization,该注解用于标注哪些方法需要登录(即带有token信息)

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorization {

}

最后要讲的是如何对于token的解析,创建一个AuthorizationInterceptor权限拦截器,在请求处理之前
我们拦截到token,对token的真实性及失效性进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@Component
public class AuthorizationInterceptor implements HandlerInterceptor {

@Autowired
private JwtUtil jwt;

@Autowired
private TokenService tokenService;

private final Logger logger = LoggerFactory.getLogger(this.getClass());

/**
* 在请求处理之前进行调用
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}

HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//从header中取出token
String authorization = request.getHeader(Constant.AUTHORIZATION);
//验证token
if(tokenService.checkToken(authorization)){//token验证成功
TokenModel tokenModel=tokenService.parseToken(authorization);
if(tokenModel!=null){
request.setAttribute(Constant.CURRENT_USER_ID, tokenModel.getUserId());
return true;
}
}
//如果验证token失败,并且方法注明了Authorization,返回401错误
if (method.getAnnotation(Authorization.class) != null){
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}

/**
* 请求处理之后进行调用,但是在视图被渲染之前
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

/**
* 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}
}

最后就是对刚刚创建的拦截器进行注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebMvc
public class InterceptorConfiguration extends WebMvcConfigurerAdapter {
@Bean
public AuthorizationInterceptor authorizationInterceptor(){
return new AuthorizationInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor())
.addPathPatterns("/**");

}
}

写在最后

到此为止,我们已经创建了一个比较简易的能够支撑轻量级的APP服务端,然后根据我们的业务,在进行扩充就可以了。

陋室铭

App项目之服务进阶篇

发表于 2016-12-17 | 分类于 Java | 阅读次数

接着上一篇文章App项目之服务基础篇,继续探讨我们App服务端的搭建与配置。今天的主题是配置,上一篇我们已经使用Spring boot搭建了一个Restful API的工程,那么在此基础上进行一些改进和配置。

  • 返回参数的格式统一化
  • 错误处理的统一配置
  • Redis的配置
  • 分页查询的配置
  • 文件上传
  • Swagger2的配置,生成Restful API文档

返回参数的格式统一化

对于服务端参数的格式的统一化,有助于App端的解析,对于双方的接口调试能够达到事倍功半的效果。返回参数的格式我是这样设置的:每个Controller中的请求方法返回值都采用的是ResponseEntity这个Spring内部的实体类,
此外我还自己定义一个ApiResponse的实体类,包括code,msg,data,datas,timestamp这几个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ApiResponse<T> {

private String code;//返回码,0000:成功,0001:缺少参数, 0002 请求错误
private String msg;//返回信息
private T data;//单个对象
private PageInfo<T> datas;//多个对象,注此处的PageInfo是Mybatis分页库中的实体,下文会介绍
private long timestamp;//系统时间戳

//构造函数,初始化code和msg
public ApiResponse(String msg, String code) {
this.code = code;
this.msg = msg;
this.timestamp=System.currentTimeMillis();
}

public ApiResponse(String msg) {
this.code = "0000";
this.msg = msg;
this.timestamp=System.currentTimeMillis();
}
//getter和setter此处省略了
}

另外,还定义了一个工具类ResponseUtil,便于处理返回实体的构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class ResponseUtil {


private static ResponseEntity<ApiResponse> generalOK(ApiResponse apiResponse){
return new ResponseEntity<ApiResponse>(apiResponse, HttpStatus.OK);
}

private static ResponseEntity<ApiResponse> generalUnAuthor(ApiResponse apiResponse){
return new ResponseEntity<ApiResponse>(apiResponse, HttpStatus.UNAUTHORIZED);
}

/**
* 未授权认证成功
* @param msg
* @return
*/
public static ResponseEntity<ApiResponse> unAuthor(String msg){
return generalUnAuthor(new ApiResponse(msg));
}

/**
* 成功返回(只包含msg和code)
* @param msg
* @return
*/
public static ResponseEntity<ApiResponse> success(String msg){
return generalOK(new ApiResponse(msg,Constant.RESCODE_SUCCESS));
}

/**
* 成功返回(包含msg,code,Obj)
* @param msg
* @param object
* @return
*/
public static ResponseEntity<ApiResponse> success(String msg,Object object){
ApiResponse apiResponse=new ApiResponse(msg,Constant.RESCODE_SUCCESS);
apiResponse.setData(object);
return generalOK(apiResponse);
}

/**
* 成功返回(包含msg,code,Obj)
* @param msg
* @param object
* @return
*/
public static ResponseEntity<ApiResponse> success(String msg,PageInfo object){
ApiResponse apiResponse=new ApiResponse(msg,Constant.RESCODE_SUCCESS);
apiResponse.setDatas(object);
return generalOK(apiResponse);
}

/**
* 不存在
* @return
*/
public static ResponseEntity<ApiResponse> notExist(){
ApiResponse apiResponse=new ApiResponse("不存在",Constant.RESCODE_NOEXIST);
return generalOK(apiResponse);
}

public static ResponseEntity<ApiResponse> error(){
ApiResponse apiResponse=new ApiResponse("操作失败了",Constant.RESCODE_ERROR);
return generalOK(apiResponse);
}
/**
* 请求出现异常信息
* @param msg
* @return
*/
public static ResponseEntity<ApiResponse> exception(String msg){
return generalOK(new ApiResponse(msg,Constant.RESCODE_EXCEPTION));
}

/**
* 未知异常
* @return
*/
public static ResponseEntity<ApiResponse> unKonwException(){
return exception("请稍后再试!");
}

/**
* 自定义返回code和msg
* @param msg
* @param code
* @return
*/
public static ResponseEntity<ApiResponse> custom(String msg,String code){
return generalOK(new ApiResponse(msg,code));
}
}

错误处理的统一配置

在日常开发中发生了异常,往往是需要通过一个统一的异常处理处理所有异常,来保证客户端能够收到友好的提示。
SpringBoot在页面 发生异常的时候会自动把请求转到/error,SpringBoot内置了一个BasicErrorController对异常进行统一的处理。
那么我们定义一个AppErrorController来处理异常。通常都会涉及到Html和Json请求,如果是Json请求则返回String或者ReponseEntity类型
,如果是Html请求,则返回ModelAndView的错误页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Controller
public class AppErrorController implements ErrorController{

private ErrorAttributes errorAttributes;
private final static String ERROR_PATH = "/error";

public AppErrorController(ErrorAttributes errorAttributes) {
this.errorAttributes = errorAttributes;
}


@RequestMapping(value = ERROR_PATH)
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, getTraceParameter(request));
HttpStatus status = getStatus(request);
return new ResponseEntity<Map<String, Object>>(body, status);
}


private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}

private Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}

private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode != null) {
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
}
}
return HttpStatus.INTERNAL_SERVER_ERROR;
}

@Override
public String getErrorPath() {
return ERROR_PATH;
}
}

Redis的配置

对于Redis服务器的安装此处就不介绍了,默认我们已经安装了Redis。在build.gradle中添加

1
compile 'org.springframework.boot:spring-boot-starter-data-redis:1.4.2.RELEASE'

在application.properties文件中添加对redis的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0

如果在redis服务器中存储自定义的Bean的话,我们建一个RedisObjectSerializer继承自RedisSerializer的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class RedisObjectSerializer implements RedisSerializer<Object> {

private Converter<Object, byte[]> serializer = new SerializingConverter();
private Converter<byte[], Object> deserializer = new DeserializingConverter();

static final byte[] EMPTY_ARRAY = new byte[0];

@Override
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
return EMPTY_ARRAY;
}
try {
return serializer.convert(object);
} catch (Exception ex) {
return EMPTY_ARRAY;
}
}

@Override
public Object deserialize(byte[] bytes) throws SerializationException {
if (isEmpty(bytes)) {
return null;
}
try {
return deserializer.convert(bytes);
} catch (Exception ex) {
throw new SerializationException("Cannot deserialize", ex);
}
}

private boolean isEmpty(byte[] data) {
return (data == null || data.length == 0);
}
}

Redis的配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configurable
@EnableCaching
public class RedisConfig {

@Bean
JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory();
}

@Bean
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) throws IOException {
RedisCacheManager manager = new RedisCacheManager(redisTemplate);
manager.setDefaultExpiration(Constant.DEFAULT_TOKEN_EXPIRES_TIME);
return manager;
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(jedisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new RedisObjectSerializer());
return template;
}
}

分页查询的配置

分页查询,我使用的是MyBatis的一个插件PageHelper,其中上文中提到的PageInfo这个实体类就是在PagerHelper这个库中定义的,我们来看一下它的使用。

1
compile 'com.github.pagehelper:pagehelper:4.1.6'

在项目中创建一个PageHelperConfig类,对MyBatis分页插件进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class PageHelperConfig {
@Bean
public PageHelper pageHelper(){
PageHelper pageHelper=new PageHelper();
Properties p=new Properties();
p.setProperty("offsetAsPageNum","true");
p.setProperty("rowBoundsWithCount","true");
p.setProperty("reasonable", "true");
//通过设置pageSize=0或者RowBounds.limit = 0就会查询出全部的结果
p.setProperty("pageSizeZero", "true");
pageHelper.setProperties(p);
return pageHelper;
}
}

随后在Mapper中我们定义一个查询全部的方法,在Service中实现分页查询

1
2
@Select("select * from tb_user")
List<UserModel> fetchAllUsers();
1
2
3
4
5
6
@Override
public PageInfo<UserModel> fetchAllUsers(int pageNum, int pageSize) {
PageHelper.startPage(pageNum,pageSize);
List<UserModel> userModelList=userMapper.fetchAllUsers();
return new PageInfo<>(userModelList);
}

这样我们就实现了分页查询,具体在Controller中返回的结果,我们看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"code": "string",
"data": {},
"datas": {
"endRow": 0,
"firstPage": 0,
"hasNextPage": true,
"hasPreviousPage": true,
"isFirstPage": true,
"isLastPage": true,
"lastPage": 0,
"list": [
{}
],
"navigatePages": 0,
"navigatepageNums": [
0
],
"nextPage": 0,
"orderBy": "string",
"pageNum": 0,
"pageSize": 0,
"pages": 0,
"prePage": 0,
"size": 0,
"startRow": 0,
"total": 0
},
"msg": "string",
"timestamp": 0
}

文件上传

几乎所有的App项目中都会有文件图片上传的功能,有的是单文件上传,有的是批量上传,那我们看一下在Spring boot工程下实现的文件上传功能。

  • 首先在application.properties 文件中添加服务器存储的路径

    1
    web.upload.file_path=/User/xxx/upload 具体服务器中的某一路径
  • 新建一个FileUploadController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Value("${web.upload.file_path}")
    private String uploadPath;

    @RequestMapping(value = "imgs/upload",method = RequestMethod.POST)
    public ResponseEntity handleImagesUpload(@RequestParam("imgs") MultipartFile[] files){
    ResponseEntity responseEntity=null;
    int len;
    if(files!=null&&(len=files.length)>0){
    for(int i=0;i<len;i++) {
    try {
    String fileName = files[i].getOriginalFilename();
    if (!TextUtils.isEmpty(fileName) && isImageFile(fileName)) {//判断文件名是否存在且是否为图片类型
    //创建输出文件对象
    File outFile = new File(uploadPath + File.separator +"imgs_"+ IceUtils.getUUID() + getFileType(fileName));
    //拷贝文件到输出文件对象
    FileUtils.copyInputStreamToFile(files[i].getInputStream(), outFile);
    //上传成功,写数据库等操作

    }
    } catch (Exception e) {
    logger.error("--FileUploadController--handleImagesUpload--", e);
    }
    }
    responseEntity=ResponseUtil.success("上传成功");
    }else{
    responseEntity= ResponseUtil.custom("上传的文件为空或者文件类型不符",RESCODE_PARAM_ERROR);
    }
    return responseEntity;
    }

Swagger2的配置,生成Restful API文档

Swagger2可以轻松的整合到Spring boot中,并与Spring MVC程序配合组织出强大RESTful API文档。
它既可以减少我们创建文档的工作量,同时说明内容又整合入实现代码中,让维护文档和修改代码整合为一体,
可以让我们在修改代码逻辑的同时方便的修改文档说明。另外Swagger2也提供了强大的页面测试功能来调试每个RESTful API。
图1
图2

接下来看如何使用Swagger2,首先在build.gradle 中添加swagger的库

1
2
3
//swagger生成restful api文档
compile 'io.springfox:springfox-swagger-ui:2.6.1'
compile 'io.springfox:springfox-swagger2:2.6.1'

在Apllication同目录下新建一个Swagger2的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableSwagger2
public class Swagger2 {

@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.icefire.api.controller"))
.paths(PathSelectors.any())
.build();
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Boot中使用Swagger2构建RESTful APIs")
.description("Spring Boot中使用Swagger2构建RESTful APIs")
.termsOfServiceUrl("https://blog.didispace.com/")
.version("1.0")
.build();
}
}

如上代码所示,通过@Configuration注解,让Spring来加载该类配置。再通过@EnableSwagger2注解来启用Swagger2。
再通过createRestApi函数创建Docket的Bean之后,apiInfo()用来创建该Api的基本信息(这些基本信息会展现在文档页面中)。
select()函数返回一个ApiSelectorBuilder实例用来控制哪些接口暴露给Swagger来展现,本例采用指定扫描的包路径来定义,
Swagger会扫描该包下所有Controller定义的API,并产生文档内容(除了被@ApiIgnore指定的请求)。

添加文档内容

在完成了上述配置后,其实已经可以生产文档内容,但是这样的文档主要针对请求本身,而描述主要来源于函数等命名产生,对用户并不友好,
我们通常需要自己增加一些说明来丰富文档内容。如下所示,我们通过@ApiOperation注解来给API增加说明、通过@ApiImplicitParams、@ApiImplicitParam注解来给参数增加说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

@Autowired
private UserService userService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());

@ApiOperation(value="创建用户", notes="该API用于创建新的用户")
@ApiImplicitParam(name = "userModel", value = "用户详细实体userModel", required = true, dataType = "UserModel")
@RequestMapping(value="",method= RequestMethod.POST)
public ResponseEntity<ApiResponse> createUser(@ModelAttribute UserModel userModel){
ResponseEntity<ApiResponse> responseEntity = ResponseUtil.error();
int createRes=0;
if(userModel!=null){
createRes=userService.createUser(userModel);
}
if(createRes>0){
responseEntity=ResponseUtil.success("success");
}
return responseEntity;
}

@ApiOperation(value="更新用户信息", notes="该API用于更新用户信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "userid", value = "用户userid", required = true, dataType = "String"),
@ApiImplicitParam(name = "userModel", value = "用户详细实体userModel", required = true, dataType = "UserModel")
})
@RequestMapping(value = "/{userid}",method = RequestMethod.POST)
public ResponseEntity<ApiResponse> updateUser(@PathVariable("userid") String userid,@ModelAttribute UserModel userModel){
ResponseEntity<ApiResponse> responseEntity = ResponseUtil.error();
int res=userService.updateUser(userid,userModel);
if(res>0){
responseEntity=ResponseUtil.success("success");
}
return responseEntity;
}

@ApiOperation(value="获取用户信息", notes="该API用于根据用户ID获取用户信息")
@ApiImplicitParam(name = "userid", value = "用户userid", required = true, dataType = "String")
@RequestMapping(value = "/{userid}",method = RequestMethod.GET)
public ResponseEntity<ApiResponse> fetchUserById(@PathVariable("userid") String userid){
ResponseEntity<ApiResponse> responseEntity = ResponseUtil.notExist();
UserModel userModel=userService.fetchUserByUserId(userid);
if(userModel!=null){
responseEntity= ResponseUtil.success("success",userModel);
}
return responseEntity;
}

@ApiOperation(value="获取所有的用户信息", notes="该API用于获取所有的用户信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "pageNum", value = "页码数", required = false, dataType = "Integer"),
@ApiImplicitParam(name = "pageSize", value = "一页显示的数目", required = false, dataType = "Integer")
})
@RequestMapping(value = "",method = RequestMethod.GET)
public ResponseEntity<ApiResponse> fetchAllUsers(@RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,@RequestParam(value = "pageSize", required = false, defaultValue = "10")Integer pageSize){
ResponseEntity<ApiResponse> responseEntity = null;
PageInfo<UserModel> userList=userService.fetchAllUsers(pageNum,pageSize);
if(userList!=null){
responseEntity= ResponseUtil.success("success",userList);
}else{
responseEntity=ResponseUtil.notExist();
}
return responseEntity;
}
}

完成上述代码添加上,启动Spring Boot程序,访问:https://localhost:8080/swagger-ui.html, 就能看到前文所展示的RESTful API的页面。

写在最后

到此为止,对Spring boot工程中的一些基本配置就介绍完了。下一篇会介绍登录注册,及Token的创建及验证等和权限控制这部分内容。

陋室铭

App项目之服务基础篇

发表于 2016-12-14 | 分类于 Java | 阅读次数

工作四年多来,绝大部分时间从事APP的研发工作,对于服务端有一定的了解,但是近年来更偏向于APP客户端的开发,但是还是有一颗成为全栈工程师的心,所以在短期内捡了捡曾经遗忘的东西。

技术选型

由于刚参加工作的时候,做过一段时间的Java Web,那么服务端还是采用自己熟悉而且擅长的Java。

开发工具(IDEA),因为它和android studio一脉相承,总之一句话就是熟悉。

WEB框架,本打算采用SpringMVC。但是近期看了一篇文章,关于WEB框架的分析图
WEB框架的分析图
Spring依然是采用最多框架,SpringBoot增长的趋势明显。
关于Spring Boot的介绍如下:

  • 可以创建独立的Spring应用程序
  • 嵌入式Tomcat,Jetty容器,可以选择构建jar包(内嵌Tomcat,只需执行java -jar命令),也可以选择构建war包部署
  • 简化了Maven,Gradle的配置(这两种构建方式都支持)
  • 自动化配置Spring,省去了我们自己配置XML

环境搭建

搭建SpringBoot环境,需要JDK的安装,IDEA安装,以及Mysql的安装,作为开发人员,环境的搭建是我们必备的技能,也不必多说了。

项目构建
打开IDEA(我使用的版本是2016.2),新建一个项目,在左侧菜单选择Spring Initializr,如下图:
图1
配置好自己的JDK的位置,设置Project的一些基本信息,如下图:
图2
我们选择使用gradle构建项目(开发Android项目,已经习惯了)选择Spring的一些依赖库,选择Web,MySql,MyBatis.
图3

然后设置自己的项目路径,完成项目的搭建。

在Project构建完成后,需要设置数据源,设置application.properties文件里进行设置在application.properties中添加如下内容:(数据库连接的配置)

1
2
3
4
spring.datasource.url=jdbc:mysql://localhost:3306/ice
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

代开build.gradle文件,dependencies中的内容如下:

1
2
3
4
5
6
dependencies {
compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.1.1')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('mysql:mysql-connector-java')
testCompile('org.springframework.boot:spring-boot-starter-test')
}

代码实现

在我们新建数据库中新建一张user表,sql如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`userid` varchar(32) COLLATE utf8_bin NOT NULL,
`gender` tinyint(1) DEFAULT '0' COMMENT '0:未知,1:男,2:女',
`mobile` varchar(45) COLLATE utf8_bin NOT NULL COMMENT '手机号',
`password` varchar(128) COLLATE utf8_bin NOT NULL COMMENT '密码',
`email` varchar(45) COLLATE utf8_bin DEFAULT NULL COMMENT '邮箱',
`nickname` varchar(45) COLLATE utf8_bin DEFAULT NULL COMMENT '昵称',
`avatar` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT '头像',
`bio` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT '个人简介',
`blog` varchar(128) COLLATE utf8_bin DEFAULT NULL COMMENT '博客',
`createTime` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间',
`updateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`userid`),
UNIQUE KEY `mobile_unique` (`mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

然后在我们的工程文件中新建bean,mapper,service,controller四个包,用于存放实体,实体映射,业务服务,控制器,在bean中新建一个UserModel的实体类,对应数据库表tb_user表结构,在 mapper包中
新建一个UserMapper接口,方法映射为User表的操作,在service包中新建一个UserService接口以及impl包中新建一个UserServiceImpl实现类实现UserService接口,在controller中创建UserController类,
实现对外的Request API。
UserModel 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserModel {

private String userId;
private int gender;
private String mobile;
private String password;
private String email;
private String nickName;
private String avatar;
private String bio;
private String blog;
private Date createTime;
private Date updateTime;
//setter和getter方法此处略去
}

UserMapper 接口内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Mapper
public interface UserMapper {

//创建用户(用户注册)
@Insert("insert into tb_user(userid,mobile,password,createTime) value(#{userId},#{mobile},#{password},null)")
int createUser(UserModel userModel);

@Update("update tb_user set email=#{email},nickname=#{nickName},avatar=#{avatar},bio=#{bio},blog=#{blog} where userid=#{id}")
int updateUser(String id,UserModel userModel);

@Select("select * from tb_user where userid=#{userId}")
UserModel fetchUserById(String userId);

}

UserServiceImpl实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service
public class UserServiceImpl implements UserService{

@Autowired
private UserMapper userMapper;
private final Logger logger = LoggerFactory.getLogger(UserServiceImpl.this.getClass());

@Override
public int createUser(UserModel userModel) {
int result=0;
try{
userModel.setUserId(IceUtils.getUUID());
result=userMapper.createUser(userModel);
}catch (Exception e){
logger.error("--UserServiceImpl.createUser---",e);
}
return result;
}

@Override
public int updateUser(String userid,UserModel userModel) {
int result=0;
try{
result=userMapper.updateUser(userid,userModel);
}catch (Exception e){
logger.error("--UserServiceImpl.updateUser---",e);
}
return result;
}

@Override
public UserModel fetchUserByUserId(String userId) {
UserModel userModel=null;
try{
userModel=userMapper.fetchUserById(userId);
}catch (Exception e){
logger.error("--UserServiceImpl.fetchUserByUserId---",e);
}
return userModel;
}
}

UserController的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

@Autowired
private UserService userService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());

@RequestMapping(value="",method= RequestMethod.POST)
public String createUser(@ModelAttribute UserModel userModel){
String result="fail";
int createRes=0;
if(userModel!=null&&userModel.getUserId()==null){
createRes=userService.createUser(userModel);
}else{
createRes=userService.updateUser(userModel.getUserId(),userModel);
}

if(createRes>0){
result="success";
}
return result;
}

@RequestMapping(value = "/{userid}",method = RequestMethod.POST)
public String updateUser(@PathVariable("userid") String userid,@ModelAttribute UserModel userModel){

int res=userService.updateUser(userid,userModel); if(res>0){
result="success";
}
return result;
}
@RequestMapping(value = "/{userid}",method = RequestMethod.GET)
public UserModel fetchUserById(@PathVariable("userid") String userid){
UserModel userModel=null;
userModel=userService.fetchUserByUserId(userid);
return userModel;
}
}

到此为止,一个简单的为APP或着Web后台提供Restful API的服务端搭建完成了,我们打开Application文件,右键运行该文件。
然后打开postman,测试我们的接口

  • 创建用户
    图4
    图5
  • 查询用户
    图6

写在最后

至此,简单的服务端的开发准备完成了,下一篇文章将会对服务端,标准化API输出,Restful API文档的输出,以及登录时Token生成及认证一套完整的配置。



参考:Keegan小钢

1…56
icefire

icefire

Stick with it,and keep moving.

53 日志
15 分类
21 标签
GitHub
© 2016 - 2021 icefire 辽ICP备16011524号-1
由 Hexo 强力驱动
主题 - NexT.Pisces