RESTful API认证方式大致有以下4种:
Basic Authentication
HTTP Basic authentication is described in RFC 2617. It’s a simple username/password scheme. 将用户名与密码进行Base64转码,但这种转码是可逆的。某些爬虫工具可能会获取这些请求信息,直接获取用户的账号和密码,如果采用HTTPS方式发送请求,每次请求和响应会被SSL加密,爬虫无法获取这些信息。另一个问题,由于API通常不能信任用户使用的客户端,如果用户在多个设备(平板、电脑、手机)中登录了这个API服务,其中一个设备出现安全问题,需要修改密码,那么其他设备也需要重新登录才行。为了解决第二个问题,需要对每个设备给予不同的证书。
OAuth
OAuth 是一种授权框架,能够让应用通过HTTP 服务获取有限的访问,访问用户账号信息,例如Facebook, GitHub, DigitalOcean都采用该技术。它可以委托认证服务授权第三方应用访问自己的账号信息。OAuth2 相比OAuth 1,可以在PC端、移动端设备上使用。
OAuth 定义了四种角色:
1)资源所属者,User, 拥有该资源的人,拥有Application所访问资源的权限。
2)客户端, Application, 需要访问用户账号信息的应用。
3)资源服务器, API。
4)授权服务器, API。
Token Authentication
JWT( JSON Web Token), 是一种以Base64编码json对象的token,加密,紧凑且自成一体(self-contained),用于在网络中两个节点之间传递信息。开源标准(RFC 7519)
JWT由三个部分组成:
1)Header, 定义加密算法类型(例如:HS256)、定义类型(JWT)。
2)Payload, 定义token携带的主要信息。
3)Signature, 创建token的签名。
OpenID
OpenID是一个去中心化的网上身份认证系统。对于支持OpenID的网站,用户不需要记住像用户名和密码这样的传统验证标记。取而代之的是,他们只需要预先在一个作为OpenID身份提供者(identity provider, IdP)的网站上注册。OpenID是去中心化的,任何网站都可以使用OpenID来作为用户登录的一种方式,任何网站也都可以作为OpenID身份提供者。OpenID既解决了问题而又不需要依赖于中心性的网站来确认数字身份。
我们使用JWT不仅仅是因为它简单易用,更有着很多优点:
1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
4.不需要在服务端保存会话信息,特别适用于分布式微服务。
首先要把JWT的maven坐标引入项目的pom文件中:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
生成token:
package com.example.demo.service.impl;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.example.demo.model.User;
import org.springframework.stereotype.Service;
@Service
public class TokenService {
public String getToken(User user) {
// token的生成方法
// Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。
// withAudience()存入需要保存在token的信息,这里我把用户ID存入token中。
String token = "";
token = JWT.create().withAudience(user.getId().toString()).sign(Algorithm.HMAC256(user.getPassword()));
return token;
}
}
接着写个自定义注解,标注在方法上用来进行生成token和验证token等操作:
package com.example.demo.util;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Target:注解的作用目标
* @Target(ElementType.TYPE)——接口、类、枚举、注解
* @Target(ElementType.FIELD)——字段、枚举的常量
* @Target(ElementType.METHOD)——方法
* @Target(ElementType.PARAMETER)——方法参数
* @Target(ElementType.CONSTRUCTOR) ——构造函数
* @Target(ElementType.LOCAL_VARIABLE)——局部变量
* @Target(ElementType.ANNOTATION_TYPE)——注解
* @Target(ElementType.PACKAGE)——包
*
* @Retention:注解的保留位置 RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
* RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
* RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。
* @Document:说明该注解将被包含在javadoc中
* @Inherited:说明子类可以继承父类中的该注解
*
*
* @author zf
*
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
然后自定义一个拦截器,拦截所有方法,判断是否拥有上一步的注解,有则生成,验证token:
package com.example.demo.interceptor;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import com.example.demo.util.PassToken;
import com.example.demo.util.UserLoginToken;
/**
* 自定义的拦截器
* @author onegis
*
*/
public class AuthenticationInterceptor implements HandlerInterceptor{
@Autowired
UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object)
throws Exception {
String token = request.getHeader("token");
// 如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method = handlerMethod.getMethod();
//检查有没有需要用户权限的注解 @UserLoginToken
if(method.isAnnotationPresent(UserLoginToken.class)){
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if(userLoginToken.required()){
//开始认证
if(token==null){
throw new RuntimeException("没有token,请重新登录!");
}
//拿到token中的userId
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (Exception e) {
throw new RuntimeException("401");
}
User user = userService.findUserById(Integer.parseInt(userId));
if(user==null){
throw new RuntimeException("用户不存在,请重新登录!");
}
//验证token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (Exception e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
}
将自己写的拦截器注入到拦截器配置中:
package com.example.demo.interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器配置
* 将自定义的拦截器注入到配置中。
* @author onegis
*
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有请求,判断是否有@UserLoginToken注解,决定是否需要验证。
registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/*/*");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor(){
return new AuthenticationInterceptor();
}
}
最后就是在controller层调用了:
@RequestMapping("/login")
@ResponseBody
public Object login(@RequestBody User user) {
JSONObject jsonObject = new JSONObject();
User userForBase = userService.findUserByUserName(user);
if (userForBase == null) {
jsonObject.put("message", "登录失败,用户不存在!");
return jsonObject;
} else {
if (!userForBase.getPassword().equals(user.getPassword())) {
jsonObject.put("message", "密码错误!");
return jsonObject;
} else {
String token = tokenService.getToken(userForBase);
jsonObject.put("token", token);
jsonObject.put("user", userForBase);
return jsonObject;
}
}
}
@ResponseBody
@UserLoginToken
@RequestMapping("/getMessage")
public String getMessage() {
return "通过验证!";
}