乐优商城:笔记(十二):鉴权微服务——授权
文章目录
- 1 创建授权中心
- 1.1 创建父module
- 1.2 授权服务的通用模块:ly-auth-common
- 1.3 授权服务的业务模块:ly-auth-service
- 1.3.1 引入依赖
- 1.3.2 配置文件
- 1.3.3 启动类
- 1.3.4 修改路由
1 创建授权中心
授权中心的主要职责:
- 用户鉴权: 接收用户的登录请求,通过用户中心的接口进行校验,通过后生成JWT
- 使用私钥生成JWT并返回
-
原理与用户鉴权类似,但逻辑稍微复杂一些(此处我们不做实现)
因为生成JWT,解析JWT这样的行为以后在其它微服务中也会用到,因此我们会抽取成工具。我们把鉴权中心进行聚合,一个工具module,一个提供服务的module
1.1 创建父module
我们先创建父module,名称为:ly-auth-center,将pom打包方式改为pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>leyou</artifactId> <groupId>com.leyou.parent</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.service</groupId> <artifactId>ly-auth-center</artifactId> <version>1.0.0-SNAPSHOT</version> <modules> <module>ly-auth-common</module> </modules> <packaging>pom</packaging> </project>
1.2 授权服务的通用模块:ly-auth-common
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>ly-auth-center</artifactId> <groupId>com.leyou.service</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.service</groupId> <artifactId>ly-auth-common</artifactId> <version>1.0.0-SNAPSHOT</version> </project>
1.3 授权服务的业务模块:ly-auth-service
1.3.1 引入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>ly-auth-center</artifactId> <groupId>com.leyou.service</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.service</groupId> <artifactId>ly-auth-service</artifactId> <version>1.0.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.leyou.service</groupId> <artifactId>ly-auth-common</artifactId> <version>${leyou.latest.version}</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> </project>
1.3.2 配置文件
server: port: 8087 spring: application: name: auth-service eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka registry-fetch-interval-seconds: 10 instance: prefer-ip-address: true ip-address: 127.0.0.1
1.3.3 启动类
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class LyAuthApplication { public static void main(String[] args) { SpringApplication.run(LyAuthApplication.class, args); } }
1.3.4 修改路由
zuul: prefix: /api # 添加路由前缀 retryable: true routes: item-service: /item/** search-service: /search/** user-service: /user/** auth-service: /auth/**
最终结构:
2 JWT工具类
我们在
ly-auth-coomon中编写一些通用的工具类:
2.1 RSA工具类:RsaUtils
public class RsaUtils { /** * 从文件中读取公钥 * * @param filename 公钥保存路径,相对于classpath * @return 公钥对象 * @throws Exception */ public static PublicKey getPublicKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPublicKey(bytes); } /** * 从文件中读取密钥 * * @param filename 私钥保存路径,相对于classpath * @return 私钥对象 * @throws Exception */ public static PrivateKey getPrivateKey(String filename) throws Exception { byte[] bytes = readFile(filename); return getPrivateKey(bytes); } /** * 获取公钥 * * @param bytes 公钥的字节形式 * @return * @throws Exception */ public static PublicKey getPublicKey(byte[] bytes) throws Exception { X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePublic(spec); } /** * 获取密钥 * * @param bytes 私钥的字节形式 * @return * @throws Exception */ public static PrivateKey getPrivateKey(byte[] bytes) throws Exception { PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); KeyFactory factory = KeyFactory.getInstance("RSA"); return factory.generatePrivate(spec); } /** * 根据密文,生成rsa公钥和私钥,并写入指定文件 * * @param publicKeyFilename 公钥文件路径 * @param privateKeyFilename 私钥文件路径 * @param secret 生成密钥的密文 * @throws IOException * @throws NoSuchAlgorithmException */ public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); SecureRandom secureRandom = new SecureRandom(secret.getBytes()); keyPairGenerator.initialize(1024, secureRandom); KeyPair keyPair = keyPairGenerator.genKeyPair(); // 获取公钥并写出 byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); writeFile(publicKeyFilename, publicKeyBytes); // 获取私钥并写出 byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); writeFile(privateKeyFilename, privateKeyBytes); } private static byte[] readFile(String fileName) throws Exception { return Files.readAllBytes(new File(fileName).toPath()); } private static void writeFile(String destPath, byte[] bytes) throws IOException { File dest = new File(destPath); if (!dest.exists()) { dest.createNewFile(); } Files.write(dest.toPath(), bytes); } }
2.2 常量类:JwtConstans
定义了jwt中的payload的常用key
public abstract class JwtConstans { public static final String JWT_KEY_ID = "id"; public static final String JWT_KEY_USER_NAME = "username"; }
2.3 载荷:UserInfo
@Data @NoArgsConstructor @AllArgsConstructor public class UserInfo { private Long id; private String username; }
2.4 对象工具类:ObjectUtils
从jwt解析得到的数据是Object类型,转换为具体类型可能出现空指针,这个工具类进行了一些转换:
public class ObjectUtils { public static String toString(Object obj) { if (obj == null) { return null; } return obj.toString(); } public static Long toLong(Object obj) { if (obj == null) { return 0L; } if (obj instanceof Double || obj instanceof Float) { return Long.valueOf(StringUtils.substringBefore(obj.toString(), ".")); } if (obj instanceof Number) { return Long.valueOf(obj.toString()); } if (obj instanceof String) { return Long.valueOf(obj.toString()); } else { return 0L; } } public static Integer toInt(Object obj) { return toLong(obj).intValue(); } }
2.5 JWT工具类
我们已经在
ly-auth-common中引入JWT依赖:jjwt和开源的时间/日期库:joda-time
public class JwtUtils { /** * 私钥加密token * * @param userInfo 载荷中的数据 * @param privateKey 私钥 * @param expireMinutes 过期时间,单位秒 * @return * @throws Exception */ public static String generateToken(UserInfo userInfo, PrivateKey privateKey, int expireMinutes) throws Exception { return Jwts.builder() .claim(JwtConstans.JWT_KEY_ID, userInfo.getId()) .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername()) .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()) .signWith(SignatureAlgorithm.RS256, privateKey) .compact(); } /** * 私钥加密token * * @param userInfo 载荷中的数据 * @param privateKey 私钥字节数组 * @param expireMinutes 过期时间,单位秒 * @return * @throws Exception */ public static String generateToken(UserInfo userInfo, byte[] privateKey, int expireMinutes) throws Exception { return Jwts.builder() .claim(JwtConstans.JWT_KEY_ID, userInfo.getId()) .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername()) .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate()) .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey)) .compact(); } /** * 公钥解析token * * @param token 用户请求中的token * @param publicKey 公钥 * @return * @throws Exception */ private static Jws<Claims> parserToken(String token, PublicKey publicKey) { return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token); } /** * 公钥解析token * * @param token 用户请求中的token * @param publicKey 公钥字节数组 * @return * @throws Exception */ private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception { return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey)) .parseClaimsJws(token); } /** * 获取token中的用户信息 * * @param token 用户请求中的令牌 * @param publicKey 公钥 * @return 用户信息 * @throws Exception */ public static UserInfo getInfoFromToken(String token, PublicKey publicKey) throws Exception { Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); return new UserInfo( ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)), ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME)) ); } /** * 获取token中的用户信息 * * @param token 用户请求中的令牌 * @param publicKey 公钥 * @return 用户信息 * @throws Exception */ public static UserInfo getInfoFromToken(String token, byte[] publicKey) throws Exception { Jws<Claims> claimsJws = parserToken(token, publicKey); Claims body = claimsJws.getBody(); return new UserInfo( ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)), ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME)) ); } }
3 编写授权登陆接口
接下来,我们需要在
ly-auth-servcice编写一个接口,对外提供登录授权服务。基本流程如下:
- 客户端携带用户名和密码请求登录
- 授权中心调用客户中心接口,根据用户名和密码查询用户信息
- 如果用户名密码正确,能获取用户,否则登录失败
- 如果校验成功,则生成JWT并返回
3.1 生成公钥和私钥
公钥和私钥要在项目一启动时就生成
我们需要在授权中心生成真正的公钥和私钥。我们必须有一个生成公钥和私钥的secret,这个可以配置到
application.yml中:
ly: jwt: secret: ly@Login(Auth}*^31)&yun6%f3q2 # 登录校验的密钥 pubKeyPath: H:/javacode/idea/rsa/rsa.pub # 公钥地址 priKeyPath: H:/javacode/idea/rsa/rsa.pri # 私钥地址 expire: 30 # 过期时间,单位分钟 cookieName: LY_TOKEN
然后编写属性类,加载这些数据:
@Data @ConfigurationProperties(prefix = "ly.jwt") public class JwtProperties { private String secret; // 密钥 private String pubKeyPath;// 公钥 private String priKeyPath;// 私钥 private int expire;// token过期时间 private String cookieName; private PublicKey publicKey; // 公钥 private PrivateKey privateKey; // 私钥 private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class); // 对象一旦实例化后,就应该读取公钥和私钥 @PostConstruct // 构造函数执行完毕后就执行 public void init(){ try { //公钥和私钥不存在 要先生成 File pubKey = new File(pubKeyPath); File priKey = new File(priKeyPath); if (!pubKey.exists() || !priKey.exists()) { // 生成公钥和私钥 RsaUtils.generateKey(pubKeyPath, priKeyPath, secret); } // 获取公钥和私钥 this.publicKey = RsaUtils.getPublicKey(pubKeyPath); this.privateKey = RsaUtils.getPrivateKey(priKeyPath); } catch (Exception e) { logger.error("初始化公钥和私钥失败!", e); throw new RuntimeException(); } } }
@ConfigurationProperties
:
可以把读取配置文件的信息,并自动封装成实体类@PostConstruct
: 被@PostConstruct
修饰的方法会在服务器加载Servlet
的时候运行(对象一旦实例化后,就执行),并且只会被服务器调用一次,类似于Serclet
的inti()
方法。被@PostConstruc
t修饰的方法会在构造函数之后,init()方法之前运行。- 如果想在生成对象时完成某些初始化操作,而偏偏这些初始化操作又依赖于依赖注入,那么就无法在构造函数中实现。为此,可以使用
@PostConstruct
注解一个方法来完成初始化,@PostConstruct
注解的方法将会在依赖注入完成后被自动调用。
顺序:Constructor >> @Autowired >> @PostConstruct
参考资料:
注:理论上公钥私钥只生成一次,不会无限生成
3.2 controller
编写授权接口,我们接收用户名和密码,校验成功后,写入cookie中,那如何写入cookie中呢?通过
HttpServletResponse response,
HttpServletRequest request,以及
ly-common中的
CookieUtils工具类(该工具类利用的工厂模式来创建)
- 将token写入cookie — 工厂模式 httpOnly():避免别的js代码来操作你的cookie,是一种安全措施
- charset():不需要编码 因为token中没有中文
- maxAge(): cookie的生命周期,默认是-1,代表一次会话,浏览器关闭cookie就失效
为什么要用到response 和 request 呢?
- response:将cookie写入 — response中有一个方法 addCookie()
- request:cookie中有域的概念 domain 例如一个cookie只能在www.baidu.com生效,无法在别的域下生效, 给cookie绑定一个域,防止别的网站访问你的cookie,也是一种安全措施
@Value("${ly.jwt.cookieName}") private String cookieName; @PostMapping("login") public ResponseEntity<Void> login(// 无需给前端浏览器返回token,token只有后端才需要使用,并且将token保存到cookie中 @RequestParam("username") String username, @RequestParam("password") String password, HttpServletResponse response, HttpServletRequest request){ // 登录功能的实现 String token = authService.login(username, password); // 将token写入cookie --- 工厂模式 CookieUtils.newBuilder(response).httpOnly().request(request).build(cookieName, token); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); }
@value
:Spring 通过注解获取*.porperties 或 *.yml文件的内容,除了xml配置外,还可以通过@value方式来获取,这种注解方式比较麻烦,需要自己写全路径,@ConfigurationProperties
方式比较简单,直接全部注入进来
3.3 UserClient
接下来我们肯定要对用户密码进行校验,所以我们需要通过FeignClient去访问
user-service微服务:
引入user-service依赖:
<dependency> <groupId>com.leyou.service</groupId> <artifactId>ly-user-interface</artifactId> <version>${leyou.latest.version}</version> </dependency>
编写FeignClient:
@FeignClient(value = "user-service") public interface UserClient extends UserApi { }
3.4 service
public String login(String username, String password) { try { // 校验用户名和密码 User user = userClient.queryUsernameAndPassword(username, password); if (user == null) { throw new LyException(ExceptionEnum.INVALID_USERNAME_PASSWORD); } // 生成token String token = JwtUtils.generateToken(new UserInfo(user.getId(), username), prop.getPrivateKey(), prop.getExpire()); return token; }catch (Exception e){ log.error("[授权中心] 用户名或者密码有误,用户名称:{}", username, e); throw new LyException(ExceptionEnum.INVALID_USERNAME_PASSWORD); } }
4 登录
接下来,我们看看登录页面,是否能够正确的发出请求。
我们在页面输入登录信息,然后点击登录:
成功跳转到了首页:
然后去看cookie:
什么都没有,为什么?——我们在下一篇博客中分析:解决cookie无法写入的问题
5 校验登录状态
当cookie无法写入的问题得到解决以后,访问首页,发现首页的顶部,登录状态依然没能判断出用户信息:
这里需要向后台发起请求,获取根据cookie获取当前用户的信息。
因为token在cookie中,因此本次请求肯定会携带token信息在头中。
5.1 controller
我们在
ly-auth-service中定义用户的校验接口,通过cookie获取token,然后校验通过返回用户信息。
- 请求方式:GET
- 请求路径:/verify
- 请求参数:无,不过我们需要从cookie中获取token信息
- 返回结果:UserInfo,校验成功返回用户信息;校验失败,则返回401
代码:
@GetMapping("verify") public ResponseEntity<UserInfo> verify( @CookieValue("LY_TOKEN") String token, HttpServletResponse response, HttpServletRequest request ){ try { // 解析token UserInfo info = JwtUtils.getInfoFromToken(token, prop.getPublicKey()); // 刷新token并重新写入 String newToken = JwtUtils.generateToken(info, prop.getPrivateKey(), prop.getExpire()); CookieUtils.newBuilder(response).httpOnly().request(request).build(cookieName, newToken); return ResponseEntity.ok(info); } catch (Exception e){ throw new LyException(ExceptionEnum.NO_AUTHORIZED); } }
获取cookie并不非得从request中取出,可以使用注解
@CookieValue:
- 作用:用来获取Cookie中的值
- 参数: value:参数名称
- required:是否必须
- defaultValue:默认值
注:只要用户有操作就应该重新刷新token(重新生成token并写入),否则用户在浏览商品的时候不经意间过了30分钟有效期,准备下单时让用户重新登陆,影响用户体验。
到此为止,我们仅仅完成了对用户的授权,接下来,我们完成鉴权,见下一篇博客鉴权微服务——鉴权
- 乐优商城:笔记(十三):鉴权微服务——鉴权
- 乐优商城:笔记(十五):订单微服务:LyOrderApplication
- 乐优商城--服务(九) :授权微服务(LyAuthApplication)--前半部分
- 乐优商城--服务(四) : 上传微服务(LyUploadApplication)
- 乐优商城:笔记(十六):实现微信支付功能
- 马哥学习笔记十二——nfs服务
- 乐优商城--服务(七) : 用户中心微服务(LyUserApplication)
- 乐优商城--关于微服务的安全问题
- 乐优商城--服务(一): 注册微服务(LyRegister)
- 乐优商城--服务(二) : 网关微服务(LyGateway)
- 乐优商城学习笔记八-商品管理(修改商品)
- SMP3.0学习笔记之十二 使用AppBuilder读取OData服务的数据
- 认证鉴权与API权限控制在微服务架构中的设计与实现:授权码模式
- 乐优商城--服务(八) :短信微服务(LySmsApplication)
- 【乐优商城】springcloud微服务-项目搭建
- 乐优商城--服务--优化搜索微服务和页面微服务
- **从零开始完成微服务项目乐优商城**
- .Net Core 商城微服务项目系列(十二):使用k8s部署商城服务
- 乐优商城--服务(三) : 商品微服务(LyItemApplication)--前半部分
- 乐优商城--服务(三) : 商品微服务(LyItemApplication)--后半部分