SpringCloud商城day09 用户认证 -2021-10-17

一. 用户认证分析: 登录

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; //当前地址不需要传递令牌
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值