之前我们在做登陆功能时,会将已登录用户的信息保存在session中,通过校验session中的相关信息来判断用户是否登陆。但是在这次练习中,每个微服务均在不同的tomcat上运行,因此无法共享session信息;并且当我们的用户数量十分庞大时,所有的信息都保存在tomcat的session中显然是一个很不可取的做法。因此我们这里使用无状态登陆即服务端不保存用户的登录信息,而是让客户端自备信息,服务端只负责对这些信息进行校验。
无状态登陆流程:
我们采用JWT + RSA非对称加密来对token进行加密
JWT包含三部分数据:
Header:头部,通常头部有两部分信息:
我们会对头部进行base64加密(可解密),得到第一部分数据
声明类型,这里是JWT
加密算法,自定义
Payload:载荷,就是有效数据,一般包含下面信息:
这部分也会采用base64加密,得到第二部分数据
用户身份信息(注意,这里因为采用base64加密,可解密,因此不要存放敏感信息)
注册声明:如token的签发时间,过期时间,签发人等
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
使用RSA非对称加密的原因:
我们的私钥和加密算法只能保存在授权中心项目中,如果不采用RSA非对称加密,那就意味着只有授权中心能对token进行解析或加密。这样一来,我们每次调用微服务之前,都要先调用授权中心进行鉴权,这大大增加了授权中心的压力。如图:
采用RSA非对称加密后,私钥和加密算法仅保存在授权中心,授权中心负责使用私钥加密生成token。非对称加密中,使用私钥加密的数据,可使用公钥或私钥进行紧密,但是使用公钥进行加密的数据,只能使用私钥进行解密。我们可以将公钥颁发给我们信任的微服务,这样调用微服务时,微服务可直接解析token判断是否有效;但如果使用公钥进行加密,会导致其他持有公钥的微服务无法解析token,自然也就无法鉴权成功。这样一来,我们既确保了token的安全,又避免了授权中心被重复多次调用。如图:
下面来创建授权微服务,同样将其创建为一个聚合工程,下面包含common和service两个子工程。记得在父工程的pom文件中设置其打包方式为pom。
common工程主要用于为其他工程提供实体类和工具类,因此需要引入以下依赖:
ly-auth-center
com.leyou.service
1.0.0-SNAPSHOT
4.0.0
com.leyou.service
ly-auth-common
io.jsonwebtoken
jjwt
0.9.0
joda-time
joda-time
接下来给出工具类:
RSA工具类(根据密文生成、读、写公钥和私钥):
package com.leyou.auth.utils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
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);
}
}
JWT工具类(生成、解析token,从token中获取载荷类)
package com.leyou.auth.utils;
import com.leyou.auth.entiy.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;
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 parserToken(String token, PublicKey publicKey) {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}
/**
* 公钥解析token
*
* @param token 用户请求中的token
* @param publicKey 公钥字节数组
* @return
* @throws Exception
*/
private static Jws 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 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 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))
);
}
}
对象工具类(各类型之间的相互转换):
package com.leyou.auth.utils;
import org.apache.commons.lang3.StringUtils;
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();
}
}
常量类(用来储存载荷的key):
package com.leyou.auth.utils;
public abstract class JwtConstans {
public static final String JWT_KEY_ID = "id";
public static final String JWT_KEY_USER_NAME = "username";
}
载荷类:
package com.leyou.auth.entiy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
private Long id;
private String username;
}
common创建完成后,我们来创建service工程:
pom.xml文件如下:
ly-auth-center
com.leyou.service
1.0.0-SNAPSHOT
4.0.0
com.leyou.service
ly-auth-service
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
com.leyou.service
ly-auth-common
1.0.0-SNAPSHOT
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-configuration-processor
true
com.leyou.common
ly-common
1.0.0-SNAPSHOT
com.leyou.service
ly-user-interface
1.0.0-SNAPSHOT
启动类:
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LyAuthApplication {
public static void main(String[] args) {
SpringApplication.run(LyAuthApplication.class);
}
}
配置文件:
server:
port: 8087
spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
ly:
jwt:
secret: ly@Login(Auth}*^31)&heiMa% # 登录校验的密钥
pubKeyPath: E:/rsa/rsa.pub # 公钥地址
priKeyPath: E:/rsa/rsa.pri # 私钥地址
expire: 30 # 过期时间,单位分钟
cookieName: LY_TOKEN
想要使用配置文件中的配置,我们需要添加一个配置类,并且我们最好在服务启动时将私钥和公钥直接加载进内存,因此配置类如下:
package com.leyou.auth.config;
import com.leyou.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;
@ConfigurationProperties(prefix = "ly.jwt")
@Data
public class JwtProperties {
private String secret;
private String pubKeyPath;
private String priKeyPath;
private int expire;
private String cookieName;
private PublicKey publicKey;
private PrivateKey privateKey;
// 对象实例化后,就应该读取公钥和私钥
// 下面的注解,使方法在构造函数执行完毕后执行
@PostConstruct
public void init() throws Exception {
File pubPath = new File(pubKeyPath);
File priPath = new File(priKeyPath);
if(!pubPath.exists() || !priPath.exists()) {
// 如果不存在,就生成公钥和私钥
RsaUtils.generateKey(pubKeyPath,priKeyPath,secret);
}
// 读取公钥和私钥
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
}
}
使用feign调用用户微服务来判断用户名和密码是否正确(继承用户微服务提供的接口):
package com.leyou.auth.client;
import com.leyou.user.api.UserApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("user-service")
public interface UserClient extends UserApi{
}
web层,注意我们在verify方法中使用了@CookieValue注解,后面括号内添加cookie的名称,这样我们可以直接取出相应的cookie。另外,我们通过verify方法每做一次验证都应刷新token的有效时间,以防用户在浏览商城时因时间到达而造成的被迫重新登录:
package com.leyou.auth.web;
import com.leyou.auth.config.JwtProperties;
import com.leyou.auth.entiy.UserInfo;
import com.leyou.auth.service.AuthService;
import com.leyou.auth.utils.JwtUtils;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.utils.CookieUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
@EnableConfigurationProperties(JwtProperties.class)
public class AuthController {
@Autowired
private AuthService authService;
@Autowired
private JwtProperties prop;
// 登陆授权功能
@PostMapping("/login")
public ResponseEntity login(
@RequestParam("username") String username, @RequestParam("password") String password,
HttpServletResponse response, HttpServletRequest request) {
String token = authService.login(username, password);
// 写入cookie
CookieUtils.setCookie(request, response, prop.getCookieName(), token);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
@GetMapping("/verify")
public ResponseEntity verify(
@CookieValue("LY_TOKEN") String token, HttpServletResponse response, HttpServletRequest request) {
if (StringUtils.isBlank(token)) {
throw new LyException(ExceptionEnum.UNAUTHORIZED);
}
try {
// 解析token
UserInfo info = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
// 刷新token生存时间
String newToken = JwtUtils.generateToken(info, prop.getPrivateKey(), prop.getExpire());
CookieUtils.setCookie(request, response, prop.getCookieName(), newToken);
return ResponseEntity.ok(info);
} catch (Exception e) {
// token未授权或已经过期
throw new LyException(ExceptionEnum.UNAUTHORIZED);
}
}
}
CookieUtils是我们自己实现的cookie工具类,具体代码如下:
package com.leyou.common.utils;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.font.TrueTypeFont;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
*
* Cookie 工具类
*
*/
@Slf4j
public final class CookieUtils {
protected static final Logger logger = LoggerFactory.getLogger(CookieUtils.class);
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null){
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
logger.error("Cookie Decode Error.", e);
}
return retValue;
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null){
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
logger.error("Cookie Decode Error.", e);
}
return retValue;
}
/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置Cookie的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}
/**
* 设置Cookie的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
* 删除Cookie带cookie域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage
* cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request)// 设置域名的cookie
cookie.setDomain(getDomainName(request));
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
logger.error("Cookie Encode Error.", e);
}
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage
* cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request)// 设置域名的cookie
cookie.setDomain(getDomainName(request));
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
logger.error("Cookie Encode Error.", e);
}
}
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
serverName = serverName.toLowerCase();
serverName = serverName.substring(7);
final int end = serverName.indexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
service层:
package com.leyou.auth.service;
import com.leyou.auth.client.UserClient;
import com.leyou.auth.config.JwtProperties;
import com.leyou.auth.entiy.UserInfo;
import com.leyou.auth.utils.JwtUtils;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.user.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@EnableConfigurationProperties(JwtProperties.class)
public class AuthService {
@Autowired
private UserClient userClient;
@Autowired
private JwtProperties prop;
public String login(String username, String password) {
// 校验用户名和密码
try {
User user = userClient.queryUserByUsernameAndPassword(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("[授权中心] 生成token失败,用户名称:{}", username);
throw new LyException(ExceptionEnum.INVALID_USERNAME_PASSWORD);
}
}
}
这样我们就实现了授权的代码,但是在前端我们登陆成功后,会发现cookie并没有成功写入。这是因为cookie是不可跨域的。我们在使用nginx进行反向代理时,实际上域名已经变为了Ip地址。我们需要对nginx进行如下图所示的修改:
我们发现还是无法正确得到域名,这是因为我们的网关同样做了一次反向代理,我们在网关里面添加如下配置来解决:
zuul.add-host-header: true
zuul.sensitive-headers:
都更改后,我们发现还是没有cookie,这和我们springcloud所使用的版本有关,我们在网关的pom.xml文件中做如下改动(将Zuul的版本调到2.0.0):
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-netflix-zuul
org.springframework.cloud
spring-cloud-netflix-zuul
2.0.0.RELEASE
再次尝试终于成功了!这样我们就完成了授权功能。还需要完成鉴权。我们所有的微服务都要经过网关,所以我么直接在网关中进行鉴权即可。对网关微服务进行改造:
首先更改配置文件,将公钥地址和和cookie的key写入,另外我们还需要配置服务白名单,因为搜索微服务、页面微服务、登陆、授权微服务等都应该被允许在未登录状态下访问,所以我们在配置文件中增添如下内容:
ly:
jwt:
pubKeyPath: E:/rsa/rsa.pub # 公钥地址
cookieName: LY_TOKEN
filter:
allowPaths:
- /api/auth
- /api/search
- /api/user/register
- /api/user/check
- /api/user/code
- /api/item
要想使用这些自定义的配置,需要添加配置类,我们在这里添加两个配置类,一个用来配置公钥,一个用来配置白名单。
公钥配置类:
package com.leyou.gateway.config;
import com.leyou.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
@ConfigurationProperties(prefix = "ly.jwt")
@Data
public class JwtProperties {
private String pubKeyPath;
private String cookieName;
private PublicKey publicKey;
@PostConstruct
public void init() throws Exception {
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
白名单过滤配置类:
package com.leyou.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
@ConfigurationProperties(prefix = "ly.filter")
@Data
public class FilterProperties {
private List allowPaths;
}
接下来我们新增一个类并继承网关过滤器,重写其中的方法。因为要使用配置类中的成员变量,所以记得在类上添加注解并注入相关配置类,过滤器如下:
package com.leyou.gateway.filters;
import com.leyou.auth.entiy.UserInfo;
import com.leyou.auth.utils.JwtUtils;
import com.leyou.common.utils.CookieUtils;
import com.leyou.gateway.config.FilterProperties;
import com.leyou.gateway.config.JwtProperties;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
public class AuthFilter extends ZuulFilter{
@Autowired
private JwtProperties prop;
@Autowired
private FilterProperties filterProp;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER-1;
}
@Override
public boolean shouldFilter() {
// 获取上下文
RequestContext requestContext = RequestContext.getCurrentContext();
// 获取request
HttpServletRequest request = requestContext.getRequest();
// 获取url路径
String path = request.getRequestURI();
boolean isAllowPath = isAllowPath(path);
return !isAllowPath;
}
private boolean isAllowPath(String path) {
for (String allowPath : filterProp.getAllowPaths()) {
if(path.startsWith(allowPath)) {
return true;
}
}
return false;
}
@Override
public Object run() throws ZuulException {
// 获取上下文
RequestContext requestContext = RequestContext.getCurrentContext();
// 获取request
HttpServletRequest request = requestContext.getRequest();
// 获取cookie中的token
String token = CookieUtils.getCookieValue(request, prop.getCookieName());
try {
UserInfo info = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
} catch (Exception e) {
// 解析失败,未登录
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
}
return null;
}
}
这样完成了鉴权
用户在登陆状态和未登录状态下都可以将商品添加购物车。
未登录时,我们使用web本地存储,将商品信息写入localstorage中,此功能由前端实现,在此不进行详细记录。我们的重点是登陆后的添加购物车。我们单独创建一个微服务专门处理购物车的增删改查。
pom.xml文件:
leyou
com.leyou.parent
1.0.0-SNAPSHOT
4.0.0
com.leyou.service
ly-cart
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-data-redis
com.leyou.service
ly-auth-common
1.0.0-SNAPSHOT
org.springframework.boot
spring-boot-configuration-processor
true
com.leyou.common
ly-common
1.0.0-SNAPSHOT
因为购物车微服务需要得到用户的id和账号,所以我们配置一个springmvc的拦截器,每次有请求发来时,我们都会进行拦截,解析token得到user并将其保存。
配置文件:
server:
port: 8088
spring:
application:
name: cart-service
redis:
host: 192.168.114.129
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
ly:
jwt:
pubKeyPath: E:/rsa/rsa.pub # 公钥地址
cookieName: LY_TOKEN
启动类:
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class LyCartApplication {
public static void main(String[] args) {
SpringApplication.run(LyCartApplication.class);
}
}
购物车中存储商品的实体类:
package com.leyou.cart.pojo;
import lombok.Data;
@Data
public class Cart {
private Long skuId;// 商品id
private String title;// 标题
private String image;// 图片
private Long price;// 加入购物车时的价格
private Integer num;// 购买数量
private String ownSpec;// 商品规格参数
}
公钥相关的配置类:
package com.leyou.cart.config;
import com.leyou.auth.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.security.PublicKey;
@ConfigurationProperties(prefix = "ly.jwt")
@Data
public class JwtProperties {
private String pubKeyPath;
private String cookieName;
private PublicKey publicKey;
@PostConstruct
public void init() throws Exception {
System.out.println(pubKeyPath);
System.out.println(cookieName);
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
}
}
拦截器进行token解析后,会得到userinfo对象,我们需要把userinfo对象存入某个容器中并让其被传递到web层。这里我们使用线程来进行传递,使用threadLocal,key是当前线程value是我我们传入的对象,视图渲染完成后,我们要记得清空数据,所以我们需要继承两个方法。另外,我们应提供一个静态方法供web层方法取出threadLocal中的userinfo对象。因此拦截器类如下:
package com.leyou.cart.interceptor;
import com.leyou.auth.entiy.UserInfo;
import com.leyou.auth.utils.JwtUtils;
import com.leyou.cart.config.JwtProperties;
import com.leyou.common.utils.CookieUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInterceptor implements HandlerInterceptor{
private JwtProperties prop;
private static final ThreadLocal tl = new ThreadLocal<>();
public UserInterceptor(JwtProperties prop) {
this.prop = prop;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 获取cookie中的token
String token = CookieUtils.getCookieValue(request, prop.getCookieName());
// 解析token
try {
UserInfo user = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
// 传递user
tl.set(user);
// 放行
return true;
} catch (Exception e) {
return false;
}
}
// 此方法在试图渲染后才执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
tl.remove();
}
public static UserInfo getUserInfo() {
return tl.get();
}
}
要想使拦截器生效,我们需要新增一个配置类并实现相应接口如下:
package com.leyou.cart.config;
import com.leyou.cart.interceptor.UserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class McvConfig implements WebMvcConfigurer {
@Autowired
private JwtProperties prop;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInterceptor(prop)).addPathPatterns("/**");
}
}
web层方法:
package com.leyou.cart.web;
import com.leyou.cart.pojo.Cart;
import com.leyou.cart.service.CartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class CartController {
@Autowired
private CartService cartService;
@PostMapping
public ResponseEntity addCart(@RequestBody Cart cart) {
cartService.addCart(cart);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@GetMapping("/list")
public ResponseEntity> queryCartList() {
return ResponseEntity.ok(cartService.queryCartList());
}
@PutMapping
public ResponseEntity updateCartNumber(
@RequestParam("id") Long skuId, @RequestParam("num") Integer num) {
cartService.updateNum(skuId,num);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
@DeleteMapping("{skuId}")
public ResponseEntity deleteCart(@PathVariable("skuId") Long skuId) {
cartService.deleteCart(skuId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
service层:
package com.leyou.cart.service;
import com.leyou.auth.entiy.UserInfo;
import com.leyou.cart.interceptor.UserInterceptor;
import com.leyou.cart.pojo.Cart;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.utils.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collector;
import java.util.stream.Collectors;
@Service
public class CartService {
@Autowired
private StringRedisTemplate template;
private final static String KEY_PREFIX = "cart:uid";
public void addCart(Cart cart) {
// 获取登陆的用户
UserInfo user = UserInterceptor.getUserInfo();
// 得到 key
String key = KEY_PREFIX + user.getId();
String hashkey = cart.getSkuId().toString();
// 判断当前购物车商品是否存在
BoundHashOperations operation = template.boundHashOps(key);
if(operation.hasKey(hashkey)) {
// 数量增加
String json = operation.get(hashkey).toString();
Cart cacheCart = JsonUtils.parse(json, Cart.class);
cacheCart.setNum(cacheCart.getNum() + cart.getNum());
operation.put(hashkey,JsonUtils.serialize(cacheCart));
}else {
// 添加
operation.put(hashkey,JsonUtils.serialize(cart));
}
}
public List queryCartList() {
String key = KEY_PREFIX + UserInterceptor.getUserInfo().getId();
if(!template.hasKey(key)) {
throw new LyException(ExceptionEnum.CART_NOT_FOUND);
}
BoundHashOperations operation = template.boundHashOps(key);
List
购物车微服务完成
第六部分到此为止