用户认证分析 1). 单点登录 一处登录 , 处处运行 ; SSO —> Single Sign On 作用: A. 解决集群环境下的登录问题 ; B. 解决多套互信的系统之间的登录问题 ; ----------> 天猫 , 淘宝 , 天猫超市 , 天猫国际 ;
2). 第三方登录 QQ登录 微博登录 微信登录
认证解决方案 1). 单点登录流程
2). 第三方登录 第三方登录基本上都采用的是 Oauth2 协议 ;
Oauth2.0 流程:
3). 前端系统用户认证流程 技术点: SpringSecurity + Jwt + Redis + Oauth2
4.2 生成私钥和公钥 A. 生成秘钥证书(存储了私钥和公钥) keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou
B. 获取公钥 keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey 4.3 认证服务导入
4.4 基于私钥生成JWT令牌
@Test public void createJWT(){ //基于私钥生成jwt //1. 创建一个秘钥工厂 //1: 指定私钥的位置 ClassPathResource classPathResource = new ClassPathResource(“changgou.jks”); //2: 指定秘钥库的密码 String keyPass = “changgou”; KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,keyPass.toCharArray());
//2. 基于工厂获取私钥
String alias = "changgou";
String password = "changgou";
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray());
//将当前的私钥转换为rsa私钥
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
//3.生成jwt
Map<String,String> map = new HashMap();
map.put("company","heima");
map.put("address","beijing");// 自定义内容
Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey));
String jwtEncoded = jwt.getEncoded();
System.out.println(jwtEncoded);
}
4.5 基于公钥校验JWT令牌 String jwt = “…”; String publicKey="…"; Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey)); String claims = token.getClaims();
注意 : 公钥和私钥是成对生成的 ; 1、用户请求认证服务完成认证。 2、认证服务下发用户身份令牌,拥有身份令牌表示身份合法。 3、用户携带令牌请求资源服务,请求资源服务必先经过网关。 4、网关校验用户身份令牌的合法,不合法表示用户没有登录,如果合法则放行继续访问。 5、资源服务获取令牌,根据令牌完成授权。 6、资源服务完成授权则响应资源信息。
Oauth2入门 1.1 准备工作 oauth_client_details 表结构; 1.2 授权码模式 1.2.1 获取授权码 访问获取授权码的URL : http://localhost:9200/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost
返回授权码 : http://localhost/?code=50E3uL 1.2.2 获取令牌 URL : POST http://localhost:9200/oauth/token 参数 : A. 认证参数
B. form表单参数
grant_type : authorization_code -------> 模式 code : 50E3uL----------------------------> 授权码 redirect_uri : http://localhost ---------> 申请授权码时重定向的连接 获取到的令牌JSON格式 :
{
"access_token": xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
"token_type" : xxx,
"refresh_token" :xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
"jti":xxxxxxxxxxxxxxxx,
"expires_in":43199
}
jti : 短令牌 ; 一个jti 对应于一个access_token ; access_token : JWT令牌 ; 1.2.3 校验刷新令牌 1). 校验 GET http://localhost:9200/oauth/check_token?token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2). 刷新 申请到的JWT令牌是有有效期的 , 当jwt令牌快过期时, 可以通过refresh_token 刷新令牌 , 重置过期时间 ; POST http://localhost:9200/oauth/token
参数 : grant_type refresh_token refresh_token xxxxxxxxxxxxxxxxxxxxxx
1.3 密码模式 POST http://localhost:9200/oauth/token
参数 : 1). 认证参数
username 实际上指ClientId password 实际上指ClientSecret 2). form表单参数 区分 clientId 与 Username :
默认配置 , 用户名密码时写死的 1.4 资源服务接入认证
1、客户端请求认证服务申请令牌 2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。 3、客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer令牌。 4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。 5、令牌有效,资源服务向客户端响应资源信息 1). 引入依赖 org.springframework.cloud spring-cloud-starter-oauth2
2). 引入公钥 public.key 3). 引入配置类 @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的 @PreAuthorize 注解 public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
//公钥
private static final String PUBLIC_KEY = "public.key";
/***
* 定义JwtTokenStore
* @param jwtAccessTokenConverter
* @return
*/
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/***
* 定义JJwtAccessTokenConverter
* @return
*/
@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请求链接进行校验
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
//所有请求必须认证通过
http.authorizeRequests()
//下边的路径放行
.antMatchers(
"/user/add","/user/load/**"). //配置地址放行
permitAll()
.anyRequest().
authenticated(); //其他地址需要认证授权
}
}
携带令牌(Header —> Authorization)访问资源服务 :
2. 认证接口开发 2.1 认证流程
2.2 申请令牌测试 通过java程序, 申请令牌 (将postman中的申请令牌的接口调用, 改为java代码实现) 步骤 : 1). 组装申请令牌的URL ; 2). 组装申请令牌所需要的参数 ; 3). 错误码的处理 ; 4). 发送请求, 获取结果 ;
@SpringBootTest @RunWith(SpringRunner.class) public class ApplyTokenTest {
/**
* 通过负载均衡的方式获取指定服务的实例对象
*/
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private RestTemplate restTemplate;
@Test
public void applyTest(){
// 1). 组装申请令牌的URL ; 构建请求地址 http://localhost:9200/oauth/token
// 1.1获取user-auth 的服务实例对象
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
// 1.2通过负载均衡的方式获取uri
URI uri = serviceInstance.getUri();
String url = uri + "/oauth/token";
// 2). 组装申请令牌所需要的参数 ;
// 2.1 第一个参数 请求体的构建
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type","password");
body.add("username","itheima");
body.add("password","itheima");
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
// 2.2 第二个参数,请求头的构建 调用自定义的私有方法,将String clientId, String clientSecret 进行封装
headers.add("Authorization",this.getHttpBasic("changgou","changgou"));
HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(body,headers);
// 3). 错误码的处理 ;
//当后端出现了401,400.后端不对着两个异常编码进行处理,而是直接返回给前端 --
restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
@Override
public void handleError(ClientHttpResponse response) throws IOException {
// 如果不是401 或者400的错误,才处理
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401){
super.handleError(response);
}
}
});
// 4). 发送请求, 获取结果 ;
/**
* 第一个参数 请求的url
* 第二个参数 请求的方式
* 第三个参数封装请求的参数
*/
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
Map map = responseEntity.getBody();
System.out.println(map);
}
private String getHttpBasic(String clientId, String clientSecret) {
// 2.3http Basic认证 时将消息转换为 Authorization :Basic Y2hhbmdnb3U6Y2hhbmdnb3U=
String value = clientId + ":" + clientSecret;
// 2.4 然后将内容进行base64加密
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic " + new String(encode);
}
}
2.3 认证接口业务层实现 步骤 : 1). 申请令牌 ; 2). 组装令牌数据 ; 3). 往redis中存储令牌 ;
代码实现: @Service public class AuthServiceImpl implements AuthService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${auth.ttl}")
private long ttl;
/**
* 根据参数获取令牌
*
* @param username 用户名
* @param password 用户密码
* @param clientId 当前服务的认证id
* @param clientSecret 当前服务的认证密码
* @return
*/
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
// 1.申请令牌
// 1.2构建请求url
// 1.2.1通过负载均衡获取相应的服务对象
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
// 1.2.2根据服务对象获取请求uri
URI uri = serviceInstance.getUri();
// 1.2.3.拼接生成请求的url
String url = uri + "/oauth/token";
// 1.3.1 构建请求的请求体,设置认证的方式 记忆用户密码
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("username", username);
body.add("password", password);
// 1.3.2 设置请求头
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization", this.getHttpBasic(clientId, clientSecret));
// 1.3 构建请求参数封装对象
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
// 1.1 通过restTemplate发送请求
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
// 2.组装令牌的数据
// 2.1获取返回的令牌数据
Map map = responseEntity.getBody();
// 2.1非空校验,如果获取到的参数为空,则申请令牌失败
if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
throw new RuntimeException("申请令牌失败");
}
// 2.2将结果数据封装到AuthToken对象中
AuthToken authToken = new AuthToken();
authToken.setAccessToken((String) map.get("access_token"));
authToken.setRefreshToken((String) map.get("refresh_token"));
authToken.setJti((String)map.get("jti"));
// 3.网redis中存储令牌
// 3.1将获取到的数据中jki作为redis的key jwt作为redis的value进行存储
// 设置#token存储到redis的过期时间
stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS);
return authToken;
}
private String getHttpBasic(String clientId, String clientSecret) {
String value = clientId + ":" + clientSecret;
byte[] encode = Base64Utils.encode(value.getBytes());
return "Basic " + new String(encode);
}
}
2.4 认证接口表现层 步骤 : 1). 接收参数, 进行健壮性判定 ; 2). 调用service层方法, 申请令牌 ; 3). 需要将短令牌 jti , 存储到Cookie 中 ;
@Controller @RequestMapping("/oauth") public class AuthServiceController {
@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("/login")
@ResponseBody
public Result login(String username, String password, HttpServletResponse response){
// 1). 接收参数, 进行健壮性判定 ;
if (StringUtils.isEmpty(username)){
throw new RuntimeException("请输入用户名");
}
if (StringUtils.isEmpty(password)){
throw new RuntimeException("请输入密码");
}
// 2). 调用service层方法, 申请令牌 ;
AuthToken authToken = authService.login(username, password, clientId, clientSecret);
// 3). 需要将短令牌 jti , 存储到Cookie 中 ;
// 当再次访问其他服务的时候就会携带着cookie 键为uid 值为jti 可以通过jti 获取到令牌,
CookieUtil.addCookie(response,cookieDomain,"/","uid",authToken.getJti(),cookieMaxAge,false);
return new Result(true, StatusCode.OK,"登录成功",authToken.getJti());
}
}
2.5 动态获取用户信息 由于目前的代码中, 密码是在自定义认证类中写死的 , 我们需要动态获取用户的信息 ;
1). 定义feign远程调用接口 @FeignClient(name = “user”) public interface UserFeign { @GetMapping("/user/load/{username}") public User findUserInfo(@PathVariable(“username”) String username); }
2). user 微服务中开发该接口 @GetMapping("/load/{username}") public User findUserInfo(@PathVariable(“username”) String username){ User user = userService.findById(username); return user; }
3). auth 认证微服务中远程调用,获取用户信息
4). 在user 微服务中ResourceServerConfig类中 , 放行
3. 认证服务对接网关 3.1 网关搭建 1). pom.xml 可以直接在父工程 gateway中引入即可 ; org.springframework.cloud spring-cloud-starter-gateway
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
2). application.yml spring: application: name: gateway-web cloud: gateway: globalcors: cors-configurations: '[/]’: # 匹配所有请求 allowedOrigins: “*” #跨域处理 允许所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE routes: - id: changgou_goods_route uri: lb://goods predicates: - Path=/api/album/ ,/api/brand/,/api/cache/ ,/api/categoryBrand/,/api/category/ ,/api/para/,/api/pref/ ,/api/sku/,/api/spec/ ,/api/spu/,/api/stockBack/ ,/api/template/** filters: #- PrefixPath=/brand - StripPrefix=1
#用户微服务
- id: changgou_user_route
uri: lb://user
predicates:
- Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
filters:
- StripPrefix=1
#认证微服务
- id: changgou_oauth_user
uri: lb://user-auth
predicates:
- Path=/api/oauth/**
filters:
- StripPrefix=1
redis: host: 192.168.200.128 server: port: 8001 eureka: client: service-url: defaultZone: http://127.0.0.1:6868/eureka instance: prefer-ip-address: true management: endpoint: gateway: enabled: true web: exposure: include: true
3). 引导类 @SpringBootApplication @EnableEurekaClient public class WebGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(WebGatewayApplication.class,args);
}
}
3.2 网关过滤器 逻辑 : 1). 判定请求是否是登录请求, 如果是, 则放行 ; 2). 判定Cookie中有没有jti短令牌, 如果没有 , 则拒绝访问 ; 3). 判定Redis中有没有jwt令牌, 如果没有, 则拒绝访问 ; 4). 对请求进行增强 , 增加一个头信息 Authorization ------> Bearer xxxxxxxxxxxxx
代码实现 : @Component public class AuthFilter implements GlobalFilter, Ordered {
@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) ){
//直接放行
return chain.filter(exchange);
}
//2.从cookie中获取jti的值,如果该值不存在,拒绝本次访问
String jti = authService.getJtiFromCookie(request);
if (StringUtils.isEmpty(jti)){
//拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//3.从redis中获取jwt的值,如果该值不存在,拒绝本次访问
String jwt = authService.getJwtFromRedis(jti);
if (StringUtils.isEmpty(jwt)){
//拒绝访问
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//4.对当前的请求对象进行增强,让它会携带令牌的信息
request.mutate().header("Authorization","Bearer "+jwt);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
测试: A. 登录, 获取到令牌(Redis , Cookie) B. 调用用户微服务的接口, 查询用户信息
自定义登录页面 1.1 定义登录页面 1). pom.xml org.springframework.boot spring-boot-starter-thymeleaf
2). 引入静态资源及模板文件
3). 定义Controller @RequestMapping("/toLogin") public String toLogin(){ return “login”; }
4). 配置白名单(不登录也能够访问的资源) 在WebSecurityConfig中配置 : @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/oauth/login", “/oauth/logout”,"/oauth/toLogin","/login.html","/css/","/data/ ","/fonts/","/img/ ","/js/**"); }
5). 开启表单登录,设置登录页面 @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .httpBasic() //启用Http基本身份验证 .and() .formLogin() //启用表单身份验证 .and() .authorizeRequests() //限制基于Request请求访问 .anyRequest() .authenticated(); //其他请求都需要经过验证 //开启表单登录 http.formLogin().loginPage("/oauth/toLogin")//设置访问登录页面的路径 .loginProcessingUrl("/oauth/login");//设置执行登录操作的路径 }
测试 : 需要网关的认证过滤器中, 来放行两个链接 : /oauth/login , /oauth/toLogin 1.2 网关过滤器代码优化 由于在系统中, 有很多的URL都不需要登录就可以访问(登录url , 跳转页面url , 注册用户url , 验证码url), 如果全部在AuthFilter进行if条件判断,维护起来不方便 , 所以定义了一个工具UrlFilter ; String path = request.getURI().getPath(); if (!UrlFilter.hasAuthorize(path) ){ //直接放行 return chain.filter(exchange); }
public class URLFilter { /** * 所有需要传递令牌的地址 */ public static String filterPath = “/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/,/api/worder/ ,/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){
// 获取到每一个需要传递令牌的url的地址集合
String[] split = filterPath.replace("**", "").split(",");
for (String value : split) {
// 判断如果url以 上述的地址开头,则返回true 需要传递令牌
if (url.startsWith(value) || value.startsWith(url)){
return true;
}
}
// url不需要传递令牌
return false;
}
}
1.3页面
自动登录
忘记密码?
权限控制 2.1 用户授权 SpringSecurity 是一个安全框架 , 包含两个部分 : 认证 , 授权 ;
2.2 JWT令牌包含角色权限 注意 : 自定义认证UserDetailsServiceImpl 的返回值 UserDetails 对象, 将会包含到JWT令牌的第二部分内容 ;
2.3 权限控制 1). 资源服务配置类中开启全局方法授权
2). 方法权限控制
如果在方法中没有加 @PreAuthorize注解 , 则只需要有合法的JWT令牌就可以访问 , 不会判定权限信息; 关于用户的权限信息, 需要配置在数据库中的: