一. 用户认证分析: 登录
1. 单点登录: 一处登录, 处处登录SSO(single sign on)
(1) 流程: 用户请求 -> 微服务网关 -> 用户/订单/秒杀微服务 -> 访问认证系统 -> 查询/存储在Redis (2) 用户认证的框架: Apache Shiro CAS Spring security
2. 第三方登录: 用户第三方已有账号密码 -> 完成本系统的注册登录
(1) 第三方认证通用标准: OAUTH协议 -> OAUTH2.0 (2) 实现: 客户端访问网站 -> 客户点击微信登录 -> 网站向微信发起请求->微信返回授权给客户端 -> 客户同意授权 -> 网站向微信发送请求 -> 微信返回授权码给网站 -> 网站携带授权码向微信申请令牌 ->微信检测授权码发送令牌 -> 网站利用令牌请求微信用户信息 -> 微信检测令牌合法性响应用户信息 -> 网站显示用户信息 (3) resource owner用第三方登录 -> client请求 -> authorization server -> as返回授权页给ro -> ro授权 -> client再请求 -> as返回authorization grant -> client携带授权码请求token -> as返回token -> client携带token请求resource server -> rs想用resource -> client显示用户信息 (4)Oauth2应用: 1) 本系统 -> 访问第三方资源 2) 外部系统 -> 访问本系统 3) 前端 -> 后端 4) 微服务A -> 微服务B
二. 认证解决方案
(1) 流程: springsecurity+Oauth+JWT+Redis
用户登录 -> 访问网关 -> 用户认证微服务 -> 合法则返回JWT -> 存储JWT到cookie和redis
->用户请求服务 -> 访问网关 -> 网关认证JWT(redis/cookie) -> 网关携带令牌跳转微服务 -> 微服务
校验令牌
三. JWT令牌
1. 优点: JSON, 令牌自定义扩展, 非对称加密+数字签名, 微服务不需要认证服务授权
2. 缺点: 令牌长, 存储大
3. 组成: 头部(typ)alg+负载(自定义扩展)+签名(base62Header.base64paylaod, secret)
4. 私钥(secret)+公钥生成:
(1) 生成密钥证书changgou.jks: keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou Keytool 是一个java提供的证书管理工具 -alias:密钥的别名 -keyalg:使用的hash算法 -keypass:密钥的访问密码 -keystore:密钥库文件名,changgou.jks保存了生成的证书 -storepass:密钥库的访问密码 (2) 查询证书: keytool -list -keystore changgou.jks (3) 加密工具包OpenSSL导出加密工具包 -配置环境变量 -keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey -复制公钥-----BEGIN PUBLIC KEY----- -public.key
四. Oauth2入门
1. 认证服务: changgou_user_oauth
(1)com.changgou.oauth.config组成: -认证服务器配置类ServerConfig: 数据源/令牌转换器/security接口/认证管理器/令牌持久化接口 -令牌信息转换类AuthenticationConverter: -UserDetailService接口: -WebSecurityConfig类: 安全认证配置 (2) yml配置类: 自定义属性 auth: ttl: 3600 #token存储到redis的过期时间 clientId: changgou clientSecret: changgou cookieDomain: localhost cookieMaxAge: -1 (3) 基于私钥 -> 生成jwt: 秘钥库位置-> 秘钥库密码-> 秘钥工厂-> 获取私钥(别名,密码)-> 转换RSA私钥-> 作为jwt签名-> jwt对象 Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey)); String jwtEncoded = jwt.getEncoded(); (4) 基于公钥 -> 解析jwt: Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey)); String claims = token.getClaims(); //返回jwt令牌的内容
2. 认证服务准备:
(1) 用户系统表oauth_client_details: changgou_user 表不能修改名称, oauth需要查询这个表获取数据 client-id / client_secret / scop / authorized_grant_type (2) 授权码模式: 客户端请求授权-> 认证服务返回授权页面-> 用户授权-> 认证服务返回授权码 -> 客户端携带授权码请求令牌 -> 认证服务返回令牌 -> 客户端携带令牌请求资源微服务器 -> 资源服务器验证令牌完成授权 -> 返回受保护资源 (3) 申请授权码: get请求路径oauth2/authorize获取路径 -> 查询oauth_client_details表 Get请求: http://localhost:9200/oauth/authorizeclient_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost client_id:客户端id,和授权配置类中设置的客户端id一致。 response_type:授权码模式固定为code scop:客户端范围,和授权配置类中设置的scop一致。 redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码 (4) Oauth2整合提供的令牌申请路径 -> POST http://localhost:9200/oauth/token grant_type:授权类型,填写 authorization_code,表示授权码模式 code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。 redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致rect_uri一致。 (5) Http Basic认证: client_id:client_ pass -> base64编码后放入Header (6) JWT的access_token作为值存入Redis, JTI作为键存入cookie (7) 检验令牌, 用oauth内置的接口: Get: http://localhost:9200/oauth/check_token?token= [access_token] (8) 刷新令牌 -> 重置过期时间: Post:http://localhost:9200/oauth/token grant_type: 固定为 refresh_token refresh_token:刷新令牌(注意不是access_token,而是refresh_token)
3. 密码模式: 申请令牌无需授权码 -> 直接使用账户密码
(1) POST请求测试: http://localhost:9200/oauth/token 携带参数: grant_type:密码模式授权填写password username:账号 password:密码 注意不是客户端的账号密码: client_id和client_pass (2)
五. 认证开发:
1. 授权服务Oauth2需要生成令牌: 放入私钥changgou.jks -> java key store(存放秘钥和公钥的容器
2.资源微服务解析令牌: 放入公钥pubic.key
3. 对接changgou_service_user: 用户微服务
(1) 将public.key放入src/main/resources (2) 导入依赖: spring-cloud-starter-Oauth2 (3) 添加配置类 com.changgou.user.config.ResourceServerConfig.java @Configuration //声明配置类 @EnableResourceServer //开启资源服务器 //激活方法的preAuthorize注解 @EnableGlobalMethodSecurity(prePostEnabled=true, secureEnabled=true) //校验JWT令牌 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setVerifierKey(getPubKey()); return converter; } (4) 测试: 需要添加头信息http header authorization Bearer JWT
4. 用户认证
(1) 需求: 退出-> 认证服务-> 删除JTI和JWT
登录-> 账号密码 -> 认证服务 -> Oauth2生成令牌 -> 存放JTI到cookie -> 存放JWT到Redis;
访问-> 网关服务-> 查询Cookie和Redis-> 资源页面
(2) 用户登录-> 认证服务-> 生成jwt-> 写入Redis-> 标识写入cookie (3) 用户访问-> 资源页面-> 跳转网关-> 网关通过cookie获取Token-> 查询Redis校验token (4) 用户退出-> 认证服务-> 清除token(cookie和redis)
5. changgou_user_auth的application.yml配置
auth: ttl: 1200 #(tell time left)token存储到redis的过期时间 clientId: changgou #客户端ID clientSecert: changgou #客户端秘钥 cookieDomain: localhost #Cookie保存对应域名 cookieMaxAge: -1 #Cookie过期时间, -1b表示浏览器关闭则销毁
6. 返回的JWT令牌信息: access-token, token_type, refresh_token, expires_in, scope, jti
7. Controller层:
@Controller @RequestMapping("/oauth") public class AuthController { @Autowired private AuthService authService; @Value("${auth.clientId}") private String clientId; @Value("${auth.clientSecret}") private String clientSecret; @Value("${auth.cookieDomain}") private String cookieDomain; @Value("${auth.cookieMaxAge}") private int cookieMaxAge; //跳转登录页面 @RequestMapping("/toLogin") public String toLogin(@RequestParam(value = "FROM",required = false,defaultValue = "")String from, Model model) { model.addAttribute("from",from); return "login"; } @RequestMapping("/login") @ResponseBody public Result login(String username, String password, HttpServletResponse response) { //1. 校验相关参数 if (StringUtils.isEmpty(username)) { //throw new RuntimeException("请输入用户名"); return new Result(false, StatusCode.ERROR, "登录失败"); } if (StringUtils.isEmpty(password)) { //throw new RuntimeException("请输入密码"); return new Result(false, StatusCode.ERROR, "登录失败"); } //2.申请令牌得到authtoken对象 AuthToken authToken = null; try { authToken = authService.login(username, password, clientId, clientSecret); } catch (Exception e) { e.printStackTrace(); return new Result(false, StatusCode.ERROR, "登录失败"); } //3.将jti值存入cookie this.saveJtiToCookie(authToken.getJti(), response); //4.返回结果 return new Result(true, StatusCode.OK, "登陆成功",authToken.getJti()); } //将令牌的短标识jti存放到cookie中, 用工具类CookieUtil private void saveJtiToCookie(String jti, HttpServletResponse response) { CookieUtil.addCookie(response, cookieDomain, "/", "uid",jti, cookieMaxAge, false); } }
8. UserController.java
@GetMapping("/load/{usermame}") public User findUserInfo(@PathVariable("username")String username){ User user = userService.findById(username); return user; }
9. UserFeign.java
@FeignClients(name="user") public interface UserFeign{ @GetMapping("/user/load/{username}") public User findUserInfo(@PathVariable("username")Stirng username); }
10. 开启认证服务的feign包扫描:
(1)changgou_user_oauth 导入依赖: user_api (2)启动类添加注解: @EnableFeignClients(basePackage={"com.changgou.user.feign"})
六. 认证服务对接客户端网关
1.创建changgou_gateway_web
(1)启动类: WebGatewayApplication.java (2)客户端网关全局过滤器: com.changgou.web.gateway.filter.AuFilter.java @Component public class AuthFilter implements GlobalFilter,Ordered { private static final String LOGIN_URL ="http://localhost:8001/api/oauth/toLogin"; @Autowired private AuthService authService; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); //1. 判断当前请求路径是否为登录请求, 是则放行 String path = request.getURI().getPath(); if ("/api/oauth/login".equals(path)||!UrlFilter.hasAuthorize(path)){ //直接放行 return chain.filter(exchange); } //2. 从cookie中获取jti的值, 不存在则拒绝访问 String jti = authService.getJtiFromCookie(request); if (StringUtils.isEmpty(jti)){ //拒绝访问 //response.setStatusCode(HttpStatus.UNAUTHORIZED); //return response.setComplete(); //跳转登录页面 return this.toLoginPage(LOGIN_URL+"?FROM="+request.getURI().getPath(), exchange); } //3. 从redis中获取jwt的值, 不存在则拒绝本次访问 String jwt = authService.getJtiFromRedis(jti); if (StringUtils.isEmpty(jwt)){ //拒绝访问 //response.setStatusCode(HttpStatus.UNAUTHORIZED); //return response.setComplete(); //跳转登录页面 return this.toLoginPage(LOGIN_URL+"?FROM="+request.getURI().getPath(), exchange); } //4. 对当前请求的对象进行增强, 让它携带令牌的信息 request.mutate().header("Authorization","Bearer "+jwt); return chain.filter(exchange); } //跳转登录页面 private Mono<Void> toLoginPage(String loginUrl, ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.SEE_OTHER); response.getHeaders().set("Location", loginUrl); return response.setComplete(); } //设置过滤器执行优先级, 内部值越小执行优先级越高 @Override public int getOrder() { return 0; } } (3) 业务层 AuthService.java @Service public class AuthService { @Autowired private StringRedisTemplate stringRedisTemplate; //从cookie中获取jti的值 public String getJtiFromCookie(ServerHttpRequest request) { HttpCookie httpCookie = request.getCookies().getFirst("uid"); if (httpCookie != null){ String jti = httpCookie.getValue(); //jti的值 return jti; } return null; } //从redis中获取jwt的值 public String getJtiFromRedis(String jti) { String jwt = stringRedisTemplate.boundValueOps(jti).get(); return jwt; } }
七. 自定义登录界面
(1) 添加依赖: spring-boot-starter-thymeleaf
(2) changgou_user_oauth服务 src/main/resources/templates/login.html
(3) 添加 AuthController.java
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
(4) 修改com.changgou.oauth.config.WebSecurityConfig.java
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(//先放行此路径, 测试认证服务
"/oauth/login","/oauth/logout","/oauth/toLogin",
"/login.html","/css/**","/data/**","/fonts/**", "img/**","/js/**");
}
2. 优化: 同一路径管理(需要令牌和不需要令牌)
(1) com.changgou.web.gateway.filter.UrlFilter.java -> 同一管理地址的令牌
public class UrlFilter {
//所有需要传递令牌的地址
public static String filterPath = "/api/worder/**, /api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";
public static boolean hasAuthorize(String url){
String[] split = filterPath.replace("**", "").split(","); // /api/user/
for (String value : split) {
//if (url.startsWith(value)){ //后缀**被替换后, 多余的/ 需要被去除
if (url.startsWith(value.substring(0, value.length()-2))){
return true; //当前访问地址需要传递令牌
}
}
return false; //当前地址不需要传递令牌
}
}