rest api 设计
分布式应用程序
- rest 方式 http协议, 应用更为广泛, 前台应用(app, web)通过 http协议 调用后台服务
- RestTemplate
- dubbo tcp 例如后台的几个应用之间进行调用
1. 如何开发 rest api
思想是把网络上的服务都看做一个个的资源,每个资源有一个唯一地址,可以对于这个资源进行增删改查
四种请求方式
把不同 http 的请求方式对应到不同的增删改查操作上
* get - 对资源的查询
* post - 对资源的新增 (不幂等)
* put - 对资源的修改 (幂等)
* delete - 对资源的删除
Restful 风格
Restful 风格即请求路径都一样, 通过请求方式加以区分
服务器端提供服务
@RestController
public class UserController {
@GetMapping("/user/{id}")
public User findById(@PathVariable("id") int id) {
User user = new User();
user.setId(id);
user.setUsername("张三");
user.setAge(18);
return user;
}
@PostMapping("/user")
public ResponseEntity add(@RequestBody User user) { // @RequestBody 用来将请求体内 json 格式的数据转换为 java 对象
System.out.println(user);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
headers.set("aaaa", "bbbb");
ResponseEntity response = new ResponseEntity(user, headers, HttpStatus.NOT_FOUND);
return response;
}
@PutMapping("/user/{id}")
public User update(@PathVariable("id") int id, @RequestBody User user) {
System.out.println(id);
System.out.println(user);
return user;
}
@DeleteMapping("/user/{id}")
public Integer add(@PathVariable("id") int id) {
System.out.println("删除了用户:" + id);
return id;
}
}
客户端访问服务器端
通过不同的方法访问不同的请求类型
- template.getForObject();
- template.delete();
- template.postForObject();
- template.put();
这四个方法第一个参数都为url,其中get 与post要在参数末尾传入一个返回类型的.class类型。
RestTemplate template = new RestTemplate();
User user = template.getForObject("http://localhost:8080/user/id=1", User.class);
System.out.println(user);
幂等
当给一个资源发送多次请求时,请求结果不受请求次数的影响,称之为幂等的
product 商品表
修改商品的库存 store 5
update product set store = store - 1 where id = 2 -- 非幂等
update product set store = 4 where id = 2 -- 幂等
- 新增总是不幂等的
- 但是设计修改方法时,建议设计为幂等的
jwt
json web tokens (令牌)
依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.5</version>
</dependency>
1. 创建令牌
生成一个秘钥对象
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
Base64Encoder encoder = new Base64Encoder();
System.out.println(encoder.encode(secretKey.getEncoded()));
要保证使用同一个秘钥对象。
// token 是最后生成的令牌(包含了 内容 claims, 签名(signature), 算法名称(alg))
String token = Jwts.builder()
//内容部分
.claim("sub", "1")
//过期时间
.claim("exp", "1552715553") // 秒
// 签名算法
.signWith(SignatureAlgorithm.HS256, "ZYUf0Oym0Lhi9V7f6ML6tXcv0HvGH7YnuhbGhk8Y/+U=")
.compact();
设置内容和时间可以以使用下边方法
String token = Jwts.builder()
// 内容部分
.setSubject("1") // claims("sub", "1")
.setExpiration(new Date(System.currentTimeMillis()+60*1000)) // 设置 1 分钟后过期
// 签名算法
.signWith(SignatureAlgorithm.HS256, "ZYUf0Oym0Lhi9V7f6ML6tXcv0HvGH7YnuhbGhk8Y/+U=")
.compact();
2. 验证令牌
// 解析器
Jwts.parser()
// 设置秘钥
.setSigningKey("ZYUf0Oym0Lhi9V7f6ML6tXcv0HvGH7YnuhbGhk8Y/+U=")
.parse(token);
好处:
- 无需将用户的认证信息,存储于服务器端的 session 中, 服务器端不会使用 session 了,称之为 stateless 无状态
- stateless 可以让服务器的扩展性得到极大提高
- 经常用在前后台分离的开发中
- 令牌信息放在请求头中,不存在跨域问题
格式
请求头名 值
Authorization "Bearer 令牌"
- 令牌中还可以存储其他信息
Jwts.builder().claims("名字", 值)
-
注意1,名字不要和预定义的名称冲突,sub,exp
-
注意2,不要存储敏感信息(如密码)
-
获取令牌信息
Jwt jwt = Jwts.parser()setSigningKey(秘钥)parse(令牌);
Claims c = (Claims)jwt.getBody();
c.get("名字");
应用中的例子
获取令牌响应的格式
ResponseEntity (内部包含了响应的各个组成部分)
new ResponseEntity(响应体, 响应头, 状态码);
-
状态码
- 200 正常
- 400 请求参数问题
- 401 认证失败(登录失败)
- 403 没有权限
- 404 资源不存在
- 405 请求方法不支持 (例如,只支持post,使用get访问)
-
响应头
-
响应体
- 一般在rest api 都是 json 格式
-
响应体给一个java 对象即可,最后会转为 json
-
响应头是一个map 集合,内含多个键值,有很多预定义的键值(content-type)
-
状态码 是一个枚举值 HttpStatus
-
控制器方法内返回这个 ResponseEntity 对象
给用户发令牌
@RestController
@RequestMapping("/gunsApi")
public class ApiController extends BaseController {
//固定的秘钥
public final static String key = "Ihor9JW8e9A1hG6McNAEXkLyvS6jo3LSOAK2Fl5h0Fw=";
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
/**
* 获取 token
*/
@PostMapping("/auth")
public ResponseEntity<Map<String, String>> auth(String username, String password) {
// 1. 获取用户
User dbUser = userDao.selectByAccount(username);
Map<String, String> body = new HashMap<>();
// 2. 检查用户存在
if( dbUser == null ){
body.put("message", "该用户不存在");
return new ResponseEntity<Map<String, String>>(body, HttpStatus.UNAUTHORIZED);
}
// 3. 检查密码是否正确
if( ! BCrypt.checkpw(password, dbUser.getPassword())) {
body.put("message", "密码不正确");
return new ResponseEntity<Map<String, String>>(body, HttpStatus.UNAUTHORIZED);
}
//4. 获取用户的角色信息
String roleId = dbUser.getRoleId();// 获取用户的角色, 中间用, 分隔
String[] split = roleId.split(",");
List<String> roleNames = new ArrayList<>();
for (String rid : split) {
String name = roleDao.selectName(Long.valueOf(rid));
roleNames.add(name);
}
// 5. 发放 access token, 必须包含 subject 和 过期时间
String token = Jwts.builder()
.setSubject(String.valueOf(dbUser.getUserId())) // subject 一般唯一
.setExpiration(new Date(System.currentTimeMillis() + 1800 * 1000))
.claim("name", dbUser.getName())
.claim("roleNames", roleNames.toString())
.signWith(SignatureAlgorithm.HS256, key)
.compact();
body.put("access_token", token);
return new ResponseEntity<>(body, HttpStatus.OK);
}
}
验证用户令牌
public class RestApiInteceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 如果是获取 token 路径,放行
// request.getRequestURI() 获取当前请求路径
if(request.getRequestURI().equals("/gunsApi/auth")) {
return true;
}
// 2.判断令牌是否合法
String authorization = request.getHeader("Authorization");
if(authorization == null || !authorization.startsWith("Bearer ")) {
response.setStatus(401);
response.setCharacterEncoding("utf-8");
response.getWriter().write("{\"message\":\"令牌不存在或格式不对\"}");
return false;
}
// 3.从请求头中拿令牌
String token = authorization.substring(7);
// 4. 解析 token 出异常说明被篡改,或是超时等
try {
Jwt jwt = Jwts.parser().setSigningKey(ApiController.key).parse(token);
// 获取令牌的内容部分
Claims body = (Claims) jwt.getBody();
System.out.println("用户的姓名:" + body.get("name"));
System.out.println("用户的角色:" + body.get("roleNames"));
} catch (ExpiredJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
response.setStatus(401);
response.setCharacterEncoding("utf-8");
response.getWriter().write("{\"message\":\"令牌无效\"}");
return false;
}
return true;
}
}