2). 第三方登录
QQ登录
微博登录
微信登录
2). 第三方登录
第三方登录基本上都采用的是 Oauth2 协议 ;
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 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、资源服务完成授权则响应资源信息。
返回授权码 :
http://localhost/?code=50E3uL
1.2.2 获取令牌
URL :
POST http://localhost:9200/oauth/token
参数 :
A. 认证参数
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
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 申请令牌测试
通过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 body = new LinkedMultiValueMap<>();
body.add("grant_type","password");
body.add("username","itheima");
body.add("password","itheima");
MultiValueMap headers = new LinkedMultiValueMap<>();
// 2.2 第二个参数,请求头的构建 调用自定义的私有方法,将String clientId, String clientSecret 进行封装
headers.add("Authorization",this.getHttpBasic("changgou","changgou"));
HttpEntity> 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
}
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 body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("username", username);
body.add("password", password);
// 1.3.2 设置请求头
MultiValueMap headers = new LinkedMultiValueMap<>();
headers.add("Authorization", this.getHttpBasic(clientId, clientSecret));
// 1.3 构建请求参数封装对象
HttpEntity> requestEntity = new HttpEntity<>(body, headers);
// 1.1 通过restTemplate发送请求
ResponseEntity
}
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
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-data-redis-reactive
2.1.3.RELEASE
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 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. 调用用户微服务的接口, 查询用户信息
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.2 JWT令牌包含角色权限
注意 : 自定义认证UserDetailsServiceImpl 的返回值 UserDetails 对象, 将会包含到JWT令牌的第二部分内容 ;
如果在方法中没有加 @PreAuthorize注解 , 则只需要有合法的JWT令牌就可以访问 , 不会判定权限信息;
关于用户的权限信息, 需要配置在数据库中的: