Rest API设计

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;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值