1、相关概念简单说明及总流程图
-
非对称加密算法概念
-
jwt说明
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。
jwt特性:
* 基于json,容易解析
* 可以在令牌中自定义内容,容易扩展
* 通过非对称加密算法以及签名技术,有效防止jwt被篡改
* 在不通过认证服务器的情况下可以通过资源服务器完成认证
* jwt令牌较长,占用存储空间jwt令牌结构
* Header: 包括了令牌类型以及所使用的的算法
* Payload : 存放了一些有效信息,以及自己添加的一些信息,但是不包括一些敏感的信息。
* signature: 签名部分,防止被篡改
完整形式:
{
alg: “RS256”,
typ: “JWT”
}.
{
companyId: “1”,
userpic: null,
user_name: “sss”,
scope: [
“app”
],
name: “test02”,
utype: “101002”,
id: “49”,
exp: 1593981062,
authorities: [
“course_view”,
],
jti: “aba19576-8e73-430a-81cc-5c87d73012d2”,
client_id: “ddd”
}.
[signature]
Oauth2说名
Oauth2的详细说明 -
总体流程图
2、相关的配置
Pom文件:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
Application.yml文件
#自定义配置
auth:
tokenValiditySeconds: 1200 #token存储到redis的过期时间
clientId: XcWebApp
clientSecret: XcWebApp
cookieDomain: xuecheng.com
cookieMaxAge: -1
# 鉴权服务器配置
encrypt:
key-store:
location: classpath:/xc.keystore
secret: xuechengkeystore
alias: xckey
password: xuecheng
#资源服务器配置
security:
oauth2:
client:
access-token-validity-seconds: 1200
client-id: XcWebApp
client-secret: XcWebApp
3、认证服务器配置
package cn.hegongda.xuecheng.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;
@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
//jwt令牌转换器
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
UserDetailsService userDetailsService;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
TokenStore tokenStore;
@Autowired
private CustomUserAuthenticationConverter customUserAuthenticationConverter;
//读取密钥的配置
@Bean("keyProp")
public KeyProperties keyProperties(){
return new KeyProperties();
}
@Resource(name = "keyProp")
private KeyProperties keyProperties;
//客户端配置
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(this.dataSource).clients(this.clientDetails());
@Bean
@Autowired
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory
(keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray())
.getKeyPair(keyProperties.getKeyStore().getAlias(),keyProperties.getKeyStore().getPassword().toCharArray());
converter.setKeyPair(keyPair);
return converter;
}
//授权服务器端点配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter)
.authenticationManager(authenticationManager)//认证管理器
.tokenStore(tokenStore)//令牌存储
.userDetailsService(userDetailsService);//用户信息service
}
//授权服务器的安全配置
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
// oauthServer.checkTokenAccess("isAuthenticated()");//校验token需要认证通过,可采用http basic认证
oauthServer.allowFormAuthenticationForClients()
.passwordEncoder(new BCryptPasswordEncoder())
// 开启/oauth/token_key验证端口无权限访问
.tokenKeyAccess("permitAll()")
// 开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("isAuthenticated()");
}
}
@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/userlogin","/userlogout","/userjwt");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
//采用bcrypt对密码进行编码
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic().and()
.formLogin()
.and()
.authorizeRequests().anyRequest().authenticated();
}
}
4、登陆认证申请令牌
@Autowired
private AuthService authService;
@Value("${auth.clientId}")
String clientId;
@Value("${auth.clientSecret}")
String clientSecret;
@Value("${auth.cookieDomain}")
String cookieDomain;
@Value("${auth.cookieMaxAge}")
int cookieMaxAge;
@PostMapping("/userlogin")
@Override
public LoginResult login(LoginRequest loginRequest) {
if (loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername())) {
ExceptionCast.cast(AuthCode.AUTH_ACCOUNT_NOTEXISTS);
}
if (loginRequest == null || StringUtils.isEmpty(loginRequest.getPassword())) {
ExceptionCast.cast(AuthCode.AUTH_PASSWORD_NONE);
}
String password = loginRequest.getPassword();
String username = loginRequest.getUsername();
// 申请令牌
AuthToken authToken = authService.getAuthToken(username,password,clientId,clientSecret);
// 将获取的authToken存入cookie中
saveAuthTokenToCookie(authToken.getAccess_token());
return new LoginResult(CommonCode.SUCCESS,authToken.getAccess_token());
}
/**
将accessToken的唯一标识存放到浏览器cookie中,指定域名携带该cookie
*/
private void saveAuthTokenToCookie(String access_token){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response,cookieDomain,"/","uid",access_token,cookieMaxAge,false);
}
申请令牌代码:
@Autowired
private RestTemplate restTemplate;
// 获取申请令牌的地址
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private RedisTemplate redisTemplate;
@Value("${auth.tokenValiditySeconds}")
Long tokenExpireTime;
// 用户认证申请令牌
public AuthToken getAuthToken(String username, String password, String clientId, String clientSecret) {
AuthToken authToken = applyToken(username,password,clientId,clientSecret);
// 判断令牌是否申请成功
if (authToken == null) {
ExceptionCast.cast(AuthCode.AUTH_TOKEN_GET_FAIL);
}
// 将令牌存入到redis中,并设置其过期时间
boolean b = saveTokenToRedis(authToken,tokenExpireTime);
if (!b){
ExceptionCast.cast(AuthCode.AUTH_TOKEN_SAVE_FAIL);
}
return authToken;
}
申请令牌的具体逻辑,通过密码的方式进行申请
http://localhost:40400/oauth/token?grant_type=password&username=xxxx&password=xxxx&clientId=xxxxx
private AuthToken applyToken(String username, String password, String clientId, String clientSecret){
// http://localhost:40400/oauth/token 申请令牌的地址
ServiceInstance serviceInstance = loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);
// http://127.0.0.1:40400
URI uri = serviceInstance.getUri();
String url = uri + "/oauth/token";
LinkedMultiValueMap<String,String> headers = new LinkedMultiValueMap<>();
// 请求头携带认证信息
headers.add("Authorization",getBasicCode(clientId,clientSecret));
LinkedMultiValueMap<String,String> body = new LinkedMultiValueMap<>();
// 通过密码模式获取access_token
body.add("grant_type","password");
body.add("username",username);
body.add("password",password);
body.add("client_id",clientId);
// public HttpEntity(T body, MultiValueMap<String, String> headers) {
HttpEntity<MultiValueMap<String,String>> httpEntity = new HttpEntity<>(body,headers);
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
//当响应的值为400或401时候也要正常响应,不要抛出异常
if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
super.handleError(response);
}
}
});
ResponseEntity<Map> entity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Map.class);
Map<String,String> map = entity.getBody();
// 校验返回的信息是否存在错误
if (map == null || map.get("refresh_token") == null ||
map.get("access_token") == null || map.get("jti") == null) {
String error_description = map.get("error_description");
if (error_description.startsWith("坏的")){
ExceptionCast.cast(AuthCode.AUTH_CREDENTIAL_ERROR);
} else if (error_description == null) {
ExceptionCast.cast(AuthCode.AUTH_ACCOUNT_NOTEXISTS);
}
ExceptionCast.cast(AuthCode.AUTH_TOKEN_GET_FAIL);
}
AuthToken authToken = new AuthToken();
// 对token进行设置
authToken.setAccess_token(map.get("jti"));
authToken.setJwt_token(map.get("access_token"));
authToken.setRefresh_token(map.get("refresh_token"));
return authToken;
}
头信息认证添加64位basic编码形式为: Basic clientId:clientSecret
// 获取basic64位编码
private String getBasicCode(String clientId,String clientSecret){
String string = clientId+":"+clientSecret;
// 进行64位编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic "+new String(encode);
}
将生成的token保存到redis中
private boolean saveTokenToRedis(AuthToken authToken,long time){
String json = JSON.toJSONString(authToken);
// 将token保存在redis中并设置过期时间
redisTemplate.opsForValue().set(authToken.getAccess_token(),json,time, TimeUnit.SECONDS);
// 判断是否存成功
Long expire = redisTemplate.getExpire(authToken.getAccess_token());
// 说明redis中存在
return expire > 0;
}
5、用户退出操作
用户退出操作主要两步:清空cookie,清空redis
@GetMapping("/userlogout")
public ResponseResult logout() {
// 从cookie中取出唯一标识,根据唯一标识进行删除
String uid = getCookie();
// 从redis中清除token
authService.delToken(uid);
// 从cookie中清除token
delToken(uid);
return new ResponseResult(CommonCode.SUCCESS);
}
private String getCookie(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Map<String, String> map = CookieUtil.readCookie(request, "");
if (map != null && map.get("uid") != null) {
return map.get("uid");
}
return null;
}
private void delToken(String uid) {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
// 将cookie的时间设置为0
CookieUtil.addCookie(response,cookieDomain,"/","uid",uid,0,false);
}
6、资源服务配置
资源服务器:指通过网关调用的一些其他的微服务
在微服务中引入pom依赖 ,在resources下添加公钥文件
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
package cn.hegongda.xuecheng.manage_course.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.stream.Collectors;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 进行权限校验
public class AuthenticationResourcesConfig extends ResourceServerConfigurerAdapter {
private static final String PUBLIC_KEY = "publickey.txt";//公钥
//定义JwtTokenStore,使用jwt令牌
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
//定义JJwtAccessTokenConverter,使用jwt令牌
@Bean public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
return converter;
}
/*** 获取非对称加密公钥 Key * @return 公钥 Key */
private String getPubKey() {
Resource resource = new ClassPathResource(PUBLIC_KEY);
try {
InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
BufferedReader br = new BufferedReader(inputStreamReader);
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return null;
}
}//Http安全配
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 保证swagger正常使用
.antMatchers("/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources","/swagger-resources/configuration/security",
"/swagger-ui.html","/webjars/**")
.permitAll()
.anyRequest()
.authenticated();
}
}
7、网关配置
添加pom依赖
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
application.yml进行配置
zuul:
routes:
manager-course:
path: /course/courseview/*
serviceId: xc-service-manage-course
strip-prefix: false #是否去掉前缀 /course ,false 不去掉,转发路径为 /xc-service-manage-course/course 去掉 /xc-service-manage-course/
sensitive-headers: # 是否将cookie传到下游服务器,不写将会全部传到下游服务器
ignored-headers:
在路由之前需要进行认证,自定义过滤器继承ZuulFilter
@Component
public class LoginFilter extends ZuulFilter {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* pre:可以在请求被路由之前调用
route:在路由请求时候被调用
post:在route和error过滤器之后被调用
error:处理请求时发生错误时被调用
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 通过int值来定义过滤器的执行顺序
* @return
*/
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
/**
* 用于自定义逻辑
* @return
*/
@Override
public Object run() {
// 获取request
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
HttpServletResponse response = currentContext.getResponse();
if (StringUtils.isEmpty(getJwtFromHeader(request))){
setErrorMessage(currentContext,response);
return null;
}
String accessToken = getAccessToken();
if (checkJwt(accessToken) < 0) {
setErrorMessage(currentContext,response);
return null;
}
return null;
}
// 从cookie中获取accessToken
private String getAccessToken(){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
Map<String, String> map = CookieUtil.readCookie(request, "uid");
if (map != null && map.get("uid") != null) {
return map.get("uid");
}
return null;
}
// 从cookie中取出accessToken区redis中进行校验
private Long checkJwt(String accessToken) {
if (StringUtils.isEmpty(accessToken)){
return -2L;
}
Long expire = stringRedisTemplate.getExpire(accessToken);
return expire;
}
private String getJwtFromHeader(HttpServletRequest request){
//头部取出令牌
String authorization = request.getHeader("Authorization");
if (StringUtils.isEmpty(authorization)){
return null;
}
if (!authorization.startsWith("Bearer ")) {
return null;
}
String jwt = authorization.substring(7);
if (StringUtils.isEmpty(jwt)) {
return null;
}
return jwt;
}
/**
封装错误信息
*/
private void setErrorMessage(RequestContext currentContext, HttpServletResponse response){
// 拒绝访问
currentContext.setSendZuulResponse(false);
// 设置返回码
currentContext.setResponseStatusCode(200);
// 构建响应信息
ResponseResult responseResult = new ResponseResult(CommonCode.UNAUTHENTICATED);
currentContext.setResponseBody(JSON.toJSONString(responseResult));
response.setContentType("application/json;charset=utf-8");
}
}
主要步骤:
* 从头中取出携带的令牌,判断令牌是否为空,为空则返回错误信息
* 从cookie中取出jwt的唯一标识,判断是否为空,为空,则返回错误信息
* 根据唯一标识在redis中获取jwt,判断其是否过期,过期则返回错误信息,否则,通过
完 ! 参考资料—在线学成项目