用户名和密码鉴权,使用Session保存用户鉴权结果。
使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)
自行采用Token进行鉴权
第一种就不介绍了,由于依赖Session来维护状态,也不太适合移动时代,新的项目就不要采用了。第二种OAuth的方案和JWT都是基于Token的,但OAuth其实对于不做开放平台的公司有些过于复杂。我们主要介绍第三种:JWT。
JWT是Json Web Token
的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected
中)
1.用户导航到登录页,输入用户名、密码,进行登录
2.服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成JWT Token
3.服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
4.用户得到token,存在localStorage、cookie或其它数据存储形式中。
5.以后用户请求/protected
中的API时,在请求的header中加入Authorization: Bearer xxxx(token)
。此处注意token之前有一个7字符长度的 Bearer
6.服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
7.用户取得结果
身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
优点
1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
4.不需要在服务端保存会话信息,特别适用于分布式微服务。
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。
就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT包含了三部分:
Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)
Payload 负载 (类似于飞机上承载的物品)
Signature 签名/签证
Header
JWT的头部承载两部分信息:token类型和采用的加密算法。
{
"alg": "HS256",
"typ": "JWT"
}
声明类型:这里是jwt
声明加密的算法:通常直接使用 HMAC
SHA256
加密算法是单向函数散列算法,常见的有MD5
、SHA
、HAMC
。
MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值
SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证
Payload
载荷就是存放有效信息的地方。
有效信息包含三个部分
1.标准中注册的声明
2.公共的声明
3.私有的声明
标准中注册的声明 (建议但不强制使用) :
iss: jwt签发者
sub: 面向的用户(jwt所面向的用户)
aud: 接收jwt的一方
exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
Signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
import io.jsonwebtoken.*;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author: xxm
* @description:
* @date: 2020/5/28 15:53
*/
public class JwtUtil {
/**过期时间---24 hour*/
private static final int EXPIRATION_TIME = 60*60*24;
/**自己设定的秘钥*/
private static final String SECRET = "023bdc63c3c5a4587*9ee6581508b9d03ad39a74fc0c9a9cce604743367c9646b";
/**前缀*/
public static final String TOKEN_PREFIX = "Bearer ";
/**表头授权*/
public static final String AUTHORIZATION = "Authorization";
/**
*
* @author: xxm
* 功能描述:创建Token
* @date: 2020/5/28 16:09
* @param:
* @return:
*/
public static String generateToken(String userName) {
Calendar calendar = Calendar.getInstance();
Date now = calendar.getTime();
// 设置签发时间
calendar.setTime(new Date());
// 设置过期时间
// 添加秒钟
calendar.add(Calendar.SECOND, EXPIRATION_TIME);
Date time = calendar.getTime();
HashMap<String, Object> map = new HashMap<>();
//you can put any data in the map
map.put("userName", userName);
String jwt = Jwts.builder()
.setClaims(map)
//签发时间
.setIssuedAt(now)
//过期时间
.setExpiration(time)
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
//jwt前面一般都会加Bearer
return TOKEN_PREFIX + jwt;
}
/**
*
* @author: xxm
* 功能描述: 解密Token
* @date: 2020/5/28 16:18
* @param:
* @return:
*/
public static String validateToken(String token) {
try {
// parse the token.
Map<String, Object> body = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
.getBody();
String userName = body.get("userName").toString();
return userName;
}catch (ExpiredJwtException e) {
throw e;
} catch (UnsupportedJwtException e) {
throw e;
} catch (MalformedJwtException e) {
throw e;
} catch (SignatureException e) {
throw e;
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e){
throw e;
}
}
}
Spring Security是一个基于Spring的通用安全框架,里面内容太多了,本文的主要目的也不是展开讲这个框架,而是如何利用Spring Security和JWT一起来完成API保护。所以关于Spring Secruity的基础内容或展开内容,请自行去官网学习(官网)。
import com.ggny.emep.interfac.springsecurityconfig.filter.JWTAuthenticationFilter;
import com.ggny.emep.interfac.springsecurityconfig.service.CustomUserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import javax.sql.DataSource;
/**
*
* @author: xxm
* 功能描述: SpringSecurity的配置
* @date: 2020/5/28 15:14
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 需要放行的URL
*/
public static final String[] AUTH_WHITELIST = {
"/user/login"
// other public endpoints of your API may be appended to this array
};
@Autowired
private DataSource dataSource;
@Bean
public JdbcTokenRepositoryImpl tokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//tokenRepository.setCreateTableOnStartup(true); // 启动创建表,创建成功后注释掉
return tokenRepository;
}
@Bean
UserDetailsService customUserService() { // 注册UserDetailsService 的bean
return new CustomUserServiceImpl();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()).passwordEncoder(new BCryptPasswordEncoder());
}
/**
* 配置请求拦截
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
//由于使用的是JWT,我们这里不需要csrf
.csrf().disable()
//基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
//可以匿名访问的链接
.antMatchers(AUTH_WHITELIST).permitAll()
//其他所有请求需要身份认证
.anyRequest().authenticated()
.and()
//.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JWTAuthenticationFilter(authenticationManager()));
}
这是标准的SpringSecurity配置内容,就不在详细说明。注意其中的
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
这行,将我们定义的JWT方法加入SpringSecurity的处理流程中。
重点就是配置请求拦截,由于我做的是一个接口服务,所以暂时除了登录验证,其他接口链接全部都要拦截,通过JWTAuthenticationFilter
过滤器来实现token的验证
验证用户名密码正确后,生成一个token,并将token返回给客户端
/**
* 方法名:作用:登陆校验密码
* 输入 username password 用户名,密码
* 输出:code: 状态码 1 为认证成功 0 为用户不存在 -1 为密码不一致 -2 表示程序错误
* success: true or false 执行成功或失败
* result:只在认证成功时返回,包含用户的全部信息
* messsage:
*
*
*/
@ResponseBody
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String toLogin(SysUser user) {
JSONObject json=new JSONObject();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
try {
SysUser user1 = userControllerClient.getUserInfoByLoginName(user.getUsername());
if (user1!=null) {
String dbPassWord = user1.getPassword();
if (bCryptPasswordEncoder.matches(user.getPassword(),dbPassWord)) {
//创建token
String token = JwtUtil.generateToken(user.getUsername());
json.put("success", true);
json.put("code", 1);
//json.put("result", user1);
json.put("time", DateUtil.dateToString(new Date()));
json.put("message", "登陆成功");
json.put(JwtUtil.AUTHORIZATION,token);
} else {
json.put("success", false);
json.put("code", -1);
json.put("message", "登陆失败,密码错误");
}
}else {
json.put("success", false);
json.put("code", 0);
json.put("message", "无此用户信息");
}
} catch (Exception e) {
json.put("code", -2);
json.put("success", false);
json.put("message", e.getMessage());
}
return JSON.toJSONString(json);
}
授权验证
用户一旦登录成功后,会拿到token,后续的请求都会带着这个token,服务端会验证token的合法性。
创建JWTAuthenticationFilter
类,我们在这个类中实现token的校验功能。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ggny.emep.interfac.common.JwtUtil;
import com.ggny.emep.interfac.springsecurityconfig.config.SpringSecurityConfig;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
/**
* token的校验
* 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
* 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
* 如果校验通过,就认为这是一个取得授权的合法请求
* @author xxm
*/
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String url = request.getRequestURI();
String header = request.getHeader(JwtUtil.AUTHORIZATION);
JSONObject json=new JSONObject();
//跳过不需要验证的路径
if(null != SpringSecurityConfig.AUTH_WHITELIST&&Arrays.asList(SpringSecurityConfig.AUTH_WHITELIST).contains(url)){
chain.doFilter(request, response);
return;
}
if (StringUtils.isBlank(header) || !header.startsWith(JwtUtil.TOKEN_PREFIX)) {
json.put("codeCheck", false);
json.put("msg", "Token为空");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(json));
return;
}
try {
UsernamePasswordAuthenticationToken authentication = getAuthentication(request,response);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}catch (ExpiredJwtException e) {
//json.put("status", "-2");
json.put("codeCheck", false);
json.put("msg", "Token已过期");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(json));
logger.error("Token已过期: {} " + e);
} catch (UnsupportedJwtException e) {
//json.put("status", "-3");
json.put("codeCheck", false);
json.put("msg", "Token格式错误");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(json));
logger.error("Token格式错误: {} " + e);
} catch (MalformedJwtException e) {
//json.put("status", "-4");
json.put("codeCheck", false);
json.put("msg", "Token没有被正确构造");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(json));
logger.error("Token没有被正确构造: {} " + e);
} catch (SignatureException e) {
//json.put("status", "-5");
json.put("codeCheck", false);
json.put("msg", "Token签名失败");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(json));
logger.error("签名失败: {} " + e);
} catch (IllegalArgumentException e) {
//json.put("status", "-6");
json.put("codeCheck", false);
json.put("msg", "Token非法参数异常");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(json));
logger.error("非法参数异常: {} " + e);
}catch (Exception e){
//json.put("status", "-9");
json.put("codeCheck", false);
json.put("msg", "Invalid Token");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSON.toJSONString(json));
logger.error("Invalid Token " + e.getMessage());
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request,HttpServletResponse response) {
String token = request.getHeader(JwtUtil.AUTHORIZATION);
if (token != null) {
String userName="";
try {
// 解密Token
userName = JwtUtil.validateToken(token);
if (StringUtils.isNotBlank(userName)) {
return new UsernamePasswordAuthenticationToken(userName, null, new ArrayList<>());
}
}catch (ExpiredJwtException e) {
throw e;
//throw new TokenException("Token已过期");
} catch (UnsupportedJwtException e) {
throw e;
//throw new TokenException("Token格式错误");
} catch (MalformedJwtException e) {
throw e;
//throw new TokenException("Token没有被正确构造");
} catch (SignatureException e) {
throw e;
//throw new TokenException("签名失败");
} catch (IllegalArgumentException e) {
throw e;
//throw new TokenException("非法参数异常");
}catch (Exception e){
throw e;
//throw new IllegalStateException("Invalid Token. "+e.getMessage());
}
return null;
}
return null;
}
}
该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。如果校验通过,就认为这是一个取得授权的合法请求。
这其中也包括了,token验证异常处理的返回信息
经过以上的配置,我们就完成了简单的访问权限控制和Token认证了,并添加了简单的异常处理,让我们的应用具有更好、更完善的错误提示和更安全的访问控制。