用户身份认证即用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录,指纹打卡等方式。
用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。
本项目包括多个子项目,如:学习系统,教学管理中心、系统管理中心等,为了提高用户体验性需要实现用户只认证一次便可以在多个拥有访问权限的系统中访问,这个功能叫做单点登录。
引用百度百科:单点登录(Single Sign On),简称为SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
下图是SSO的示意图,用户登录学成网一次即可访问多个系统。
作为互联网项目难免需要访问外部系统的资源,同样本系统也要访问第三方系统的资源接口,一个场景如下:
一个微信用户没有在学成在线注册,本系统可以通过请求微信系统来验证该用户的身份,验证通过后该用户便可在本系统学习,它的基本流程如下:
从上图可以看出,微信不属于本系统,本系统并没有存储微信用户的账号、密码等信息,本系统如果要获取该用户的基本信息则需要首先通过微信的认证系统(微信认证)进行认证,微信认证通过后本系统便可获取该微信用户的基本信息,从而在本系统将该微信用户的头像、昵称等信息显示出来,该用户便不用在本系统注册却可以直接学习。
什么是第三方认证(跨平台认证)?
当需要访问第三方系统的资源时需要首先通过第三方系统的认证(例如:微信认证),由第三方系统对用户认证通过,并授权资源的访问权限。
分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如:MySQL、Redis,考虑性能要求,通常存储在Redis中,如下图:
单点登录的特点是:
第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。
Oauth
协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用Oauth
认证服务,任何服务提供商都可以实现自身的Oauth
认证服务,因而Oauth
是开放的。业界提供了Oauth
的多实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而Oauth
是简易的。互网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了Oauth
认证服务,这些都足以说明Oauth
标准逐渐成为开放资源授权的标准。
黑马程序员网站使用微信认证的过程:
Oauth2.0
认证流程如下:
Oauth2
包括以下角色:
本项目采用Spring security
+ Oauth2
完成用户认证及用户授权,Spring security
是一个强大的和高度可定制的身份验证和访问控制框架,Spring security
框架集成了Oauth2
协议,下图是项目认证架构图:
授权码流程见流程图
启动项目
启动nginx
访问路径
http://localhost:40400/auth/oauth/authorize?client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost
输入账号密码,账号为:client_id
,密码为:client_secret
,下面为登录成功后的界面。
点击Authorize
按钮完成授权并跳转到学成在线首页,可以看到URL路径
后面带上了code
参数。
截图的时候忘了换
POST
请求
使用POST
请求:http://localhost:40400/auth/oauth/token
填写请求参数
grant_type:授权类型,填写authorization_code,表示授权码模式。
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转
url
,一定和申请授权码时用的redirect_uri
一致。
选择Authorization
模式
点击Send
,发送请求
access_token:访问令牌,携带此令牌访问资源。
token_type:有
MAC Token
与Bearer Token
两种类型,两种的校验算法不同,RFC 6750
建议Oauth2
采用Bearer Token
。refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
微服务即为资源服务,各个微服务中的
API
就是获取其资源的办法。
资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资源。
Http header
中添加:Authorization:Bearer
令牌。配置公钥
认证服务生成令牌采用非对称加密算法,认证服务采用私钥加密生成令牌,对外向资源服务提供公钥,资源服务使用公钥来校验令牌的合法性。
将公钥拷贝到publickey.txt
文件中,将此文件拷贝到资源服务工程的classpath
下
引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
编写配置类
package com.xuecheng.auth.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 ResourceServerConfig 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安全配置,对每个到达系统的http请求链接进行校验
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests().anyRequest().authenticated();
}
}
直接访问:http://localhost:31200/course/coursepic/list/4028e58161bd3b380161bd3bcd2f0000
,显示权限不足。
请求带上token
,再次访问。成功获取数据
修改配置类,配置需要放行的路径
//Http安全配置,对每个到达系统的http请求链接进行校验
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
.antMatchers("/v2/api‐docs", "/swagger‐resources/configuration/ui",
"/swagger‐resources", "/swagger‐resources/configuration/security",
"/swagger‐ui.html", "/webjars/**").permitAll()
.anyRequest().authenticated();
}
比较简单,主要说下流程。
client_id
和client_secret
,但是grant_type
是password
,验证客户端是否正确token
发送请求
GET auth/oauth/check_token?token=
参数
token:申请到的token
发送请求
POST auth/oauth/token
参数
grant_type:refresh_token
refresh_token:申请到的refresh_token
JWT参考这里
感觉这个介绍JWT
要全一点
keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass imxushuai-xuecheng
keytool命令参数介绍:
- alias:密钥的别名
- keyalg:使用的hash算法
- keypass:密钥的访问密码
- keystore:密钥库文件名,xc.keystore保存了生成的证书
- storepass:密钥库的访问密码
查询证书信息的命令:
keytool -list -keystore xc.keystore
若未安装openssl
,需要先安装
安装openssl
:http://slproweb.com/products/Win32OpenSSL.html
配置openssl
的环境变量
keytool ‐list ‐rfc ‐‐keystore xc.keystore | openssl x509 ‐inform pem ‐pubkey
PUBLIC KEY部分就是公钥,复制出来合并到一行
执行流程:
使用redis存储用户的身份令牌有以下作用:
package com.xuecheng.api.auth;
import com.xuecheng.framework.domain.ucenter.request.LoginRequest;
import com.xuecheng.framework.domain.ucenter.response.LoginResult;
import com.xuecheng.framework.model.response.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@Api(value = "用户认证",description = "用户认证接口")
public interface AuthControllerApi {
@ApiOperation("登录")
LoginResult login(LoginRequest loginRequest);
@ApiOperation("退出")
ResponseResult logout();
}
auth:
tokenValiditySeconds: 1200 #token存储到redis的过期时间
clientId: XcWebApp
clientSecret: XcWebApp
cookieDomain: imxushuai.com
cookieMaxAge: -1
package com.xuecheng.framework.domain.ucenter.response;
import com.google.common.collect.ImmutableMap;
import com.xuecheng.framework.model.response.ResultCode;
import io.swagger.annotations.ApiModelProperty;
import lombok.ToString;
/**
* Created by admin on 2018/3/5.
*/
@ToString
public enum AuthCode implements ResultCode {
AUTH_USERNAME_NONE(false,23001,"请输入账号!"),
AUTH_PASSWORD_NONE(false,23002,"请输入密码!"),
AUTH_VERIFYCODE_NONE(false,23003,"请输入验证码!"),
AUTH_ACCOUNT_NOTEXISTS(false,23004,"账号不存在!"),
AUTH_CREDENTIAL_ERROR(false,23005,"账号或密码错误!"),
AUTH_LOGIN_ERROR(false,23006,"登陆过程出现异常请尝试重新操作!"),
AUTH_LOGIN_APPLY_TOKEN_FAIL(false, 24001, "申请令牌失败"),
AUTH_LOGIN_TOKEN_SAVE_FAIL(false, 24002, "TOKEN保存到Redis失败"),
AUTH_LOGIN_AUTHSERVER_NOTFOUND(false, 24003, "未找到运行中的认证服务器");
//操作代码
@ApiModelProperty(value = "操作是否成功", example = "true", required = true)
boolean success;
//操作代码
@ApiModelProperty(value = "操作代码", example = "22001", required = true)
int code;
//提示信息
@ApiModelProperty(value = "操作提示", example = "操作过于频繁!", required = true)
String message;
private AuthCode(boolean success, int code, String message){
this.success = success;
this.code = code;
this.message = message;
}
private static final ImmutableMap<Integer, AuthCode> CACHE;
static {
final ImmutableMap.Builder<Integer, AuthCode> builder = ImmutableMap.builder();
for (AuthCode commonCode : values()) {
builder.put(commonCode.code(), commonCode);
}
CACHE = builder.build();
}
@Override
public boolean success() {
return success;
}
@Override
public int code() {
return code;
}
@Override
public String message() {
return message;
}
}
package com.xuecheng.auth.service;
import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.client.XcServiceList;
import com.xuecheng.framework.domain.ucenter.ext.AuthToken;
import com.xuecheng.framework.domain.ucenter.response.AuthCode;
import com.xuecheng.framework.exception.ExceptionCast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Service
public class AuthService {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthService.class);
@Value("${auth.tokenValiditySeconds}")
int tokenValiditySeconds;
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
//认证方法
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//申请令牌
AuthToken authToken = applyToken(username, password, clientId, clientSecret);
if (authToken == null) {
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLY_TOKEN_FAIL);
}
//将 token存储到redis
String access_token = authToken.getAccess_token();
String content = JSON.toJSONString(authToken);
boolean saveTokenResult = saveToken(access_token, content, tokenValiditySeconds);
if (!saveTokenResult) {
ExceptionCast.cast(AuthCode.AUTH_LOGIN_TOKEN_SAVE_FAIL);
}
return authToken;
}
//存储令牌到redis
private boolean saveToken(String access_token, String content, long ttl) {
//令牌名称
String name = "user_token:" + access_token;
//保存到令牌到redis
stringRedisTemplate.boundValueOps(name).set(content, ttl, TimeUnit.SECONDS);
//获取过期时间
Long expire = stringRedisTemplate.getExpire(name);
return expire > 0;
}
//认证方法
private AuthToken applyToken(String username, String password, String clientId, String
clientSecret) {
//选中认证服务的地址
ServiceInstance serviceInstance =
loadBalancerClient.choose(XcServiceList.XC_SERVICE_UCENTER_AUTH);
if (serviceInstance == null) {
LOGGER.error("choose an auth instance fail");
ExceptionCast.cast(AuthCode.AUTH_LOGIN_AUTHSERVER_NOTFOUND);
}
//获取令牌的url
String path = serviceInstance.getUri().toString() + "/auth/oauth/token";
//定义body
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
//授权方式
formData.add("grant_type", "password");
//账号
formData.add("username", username);
//密码
formData.add("password", password);
//定义头
MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
header.add("Authorization", httpbasic(clientId, clientSecret));
//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
//当响应的值为400或401时候也要正常响应,不要抛出异常
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
Map map = null;
try {
//http请求spring security的申请令牌接口
ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST, new HttpEntity<MultiValueMap<String, String>>(formData, header), Map.class);
map = mapResponseEntity.getBody();
} catch (RestClientException e) {
e.printStackTrace();
LOGGER.error("request oauth_token_password error: {}", e.getMessage());
e.printStackTrace();
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLY_TOKEN_FAIL);
}
if (map == null ||
map.get("access_token") == null ||
map.get("refresh_token") == null ||
map.get("jti") == null) {//jti是jwt令牌的唯一标识作为用户身份令牌
ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLY_TOKEN_FAIL);
}
AuthToken authToken = new AuthToken();
//访问令牌(jwt)
String jwt_token = (String) map.get("access_token");
//刷新令牌(jwt)
String refresh_token = (String) map.get("refresh_token");
//jti,作为用户的身份标识
String access_token = (String) map.get("jti");
authToken.setJwt_token(jwt_token);
authToken.setAccess_token(access_token);
authToken.setRefresh_token(refresh_token);
return authToken;
}
//获取httpbasic认证串
private String httpbasic(String clientId, String clientSecret) {
//将客户端id和客户端密码拼接,按“客户端id:客户端密码”
String string = clientId + ":" + clientSecret;
//进行base64编码
byte[] encode = Base64.encode(string.getBytes());
return "Basic " + new String(encode);
}
}
package com.xuecheng.auth.controller;
import com.xuecheng.api.auth.AuthControllerApi;
import com.xuecheng.auth.service.AuthService;
import com.xuecheng.framework.domain.ucenter.ext.AuthToken;
import com.xuecheng.framework.domain.ucenter.request.LoginRequest;
import com.xuecheng.framework.domain.ucenter.response.AuthCode;
import com.xuecheng.framework.domain.ucenter.response.LoginResult;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.framework.utils.CookieUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
@RestController
public class AuthController implements AuthControllerApi {
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
@Value("${auth.cookieDomain}")
private String cookieDomain;
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
@Value("${auth.tokenValiditySeconds}")
private int tokenValiditySeconds;
@Autowired
private AuthService authService;
@Override
@PostMapping("/userlogin")
public LoginResult login(LoginRequest loginRequest) {
//校验账号是否输入
if (loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername())) {
ExceptionCast.cast(AuthCode.AUTH_USERNAME_NONE);
}
//校验密码是否输入
if (StringUtils.isEmpty(loginRequest.getPassword())) {
ExceptionCast.cast(AuthCode.AUTH_PASSWORD_NONE);
}
AuthToken authToken = authService.login(loginRequest.getUsername(),
loginRequest.getPassword(), clientId, clientSecret);
//将令牌写入cookie
//访问token
String access_token = authToken.getAccess_token();
//将访问令牌存储到cookie
saveCookie(access_token);
return new LoginResult(CommonCode.SUCCESS, access_token);
}
//将令牌保存到cookie
private void saveCookie(String token) {
HttpServletResponse response = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getResponse();
//添加cookie 认证令牌,最后一个参数设置为false,表示允许浏览器获取
CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, cookieMaxAge, false);
}
@Override
@PostMapping("/userlogout")
public ResponseResult logout() {
return null;
}
}
修改WebSecurityCconfig
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/userlogin", "/userlogout", "/userjwt");
}
我这里用的redis-desktop-management