Oauth2+jwt+redis+cookie+springsecurity+springboot+springcloud(用户登录认证授权)

  1. 用户认证分析
    1). 单点登录
    一处登录 , 处处运行 ;
    SSO —> Single Sign On
    作用:
    A. 解决集群环境下的登录问题 ;
    B. 解决多套互信的系统之间的登录问题 ; ----------> 天猫 , 淘宝 , 天猫超市 , 天猫国际 ;

2). 第三方登录
QQ登录
微博登录
微信登录

  1. 认证解决方案
    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、资源服务完成授权则响应资源信息。

在这里插入图片描述

  1. 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 定义登录页面
    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页面

忘记密码?
  1. 权限控制
    2.1 用户授权
    SpringSecurity 是一个安全框架 , 包含两个部分 : 认证 , 授权 ;

在这里插入图片描述
2.2 JWT令牌包含角色权限
注意 : 自定义认证UserDetailsServiceImpl 的返回值 UserDetails 对象, 将会包含到JWT令牌的第二部分内容 ;

在这里插入图片描述
2.3 权限控制
1). 资源服务配置类中开启全局方法授权

在这里插入图片描述
2). 方法权限控制

在这里插入图片描述
在这里插入图片描述
如果在方法中没有加 @PreAuthorize注解 , 则只需要有合法的JWT令牌就可以访问 , 不会判定权限信息;
关于用户的权限信息, 需要配置在数据库中的:

在这里插入图片描述

发布了92 篇原创文章 · 获赞 3 · 访问量 2790

猜你喜欢

转载自blog.csdn.net/weixin_44993313/article/details/104502190