很早之前,听人家做淘淘商城的人一直说单点登录,但是一直不明白单点登陆是什么,看百度百科
如果是这样的话,那么我们这个应该也算是一种单点登陆的解决方案。
我们的登陆是服务端无状态的登陆
采用jwt+rsa对称式加密算法来生成一个令牌,保存在浏览器,浏览器下次可以通过携带cookie来,我们用公钥对其解密,如果能够解出其中的信息,没那么证明这个令牌是正确的,这个人已经登陆。
jwt生成的token是3部分组成的,
头部,协议信息以及加密方式
载荷,用户的信息
签名,前两部分,再加上密钥,加密生成的,用于验证整个数据的完整性和可靠性。
我们这里用到了几个工具类
RsA加密,生成公钥,私钥的方法
/** * Created by ace on 2018/5/10. * * @author HuYi.Zhang */ 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); } }
载荷中的信息,保存在一个类中
public abstract class JwtConstans { public static final String JWT_KEY_ID = "id"; public static final String JWT_KEY_USER_NAME = "username"; }
jwt生成token的方法
/** * @author: HuYi.Zhang * @create: 2018-05-26 15:43 **/ 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)) ); } }
将对象转化为各种数据类型的工具类
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(); } }
我们再创建一个实体类,来接受解析后的用户信息
public class UserInfo { private Long id; private String username;
当用户第一次请求的时候,我们会验证用户名和密码,并生成token
/** * 登陆获取令牌的方法 * @param username * @param password * @param request * @param response * @return */ @PostMapping("accredit") public ResponseEntity<Void> authorization( @RequestParam("username") String username, @RequestParam("password") String password, HttpServletRequest request, HttpServletResponse response ){ String token = this.authService.getToken(username,password); if (StringUtils.isBlank(token)){ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } //将令牌放到cookie中,httponly设置为true,防止js修改 CookieUtils.setCookie(request,response,jwtProperties.getCookieName(),token,jwtProperties.getCookieMaxAge(),null,true); return ResponseEntity.status(HttpStatus.OK).build(); }
service
/** *获取令牌的方法 * @param username * @param password * @return */ public String getToken(String username, String password) { try { ResponseEntity<User> userResponseEntity = this.userClient.queryUser(username, password); if (!userResponseEntity.hasBody()) { logger.info("用户信息不存在,{}", username); return null; } User user = userResponseEntity.getBody(); //生成令牌 UserInfo userInfo = new UserInfo(); BeanUtils.copyProperties(user, userInfo); String token = JwtUtils.generateToken(userInfo, jwtProperties.getPrivateKey(), jwtProperties.getExpire()); return token; } catch (Exception e) { logger.error("生成令牌的过程中出错"); return null; } }
这个时候还需要一个工具类,cookieUtils
/** * * Cookie 工具类 * */ public final class CookieUtils { 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,并指定编码 * @param request 请求 * @param response 响应 * @param cookieName name * @param cookieValue value * @param encodeString 编码 */ public static final void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, String encodeString) { setCookie(request,response,cookieName,cookieValue,null,encodeString, null); } /** * 生成cookie,并指定生存时间 * @param request 请求 * @param response 响应 * @param cookieName name * @param cookieValue value * @param cookieMaxAge 生存时间 */ public static final void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, Integer cookieMaxAge) { setCookie(request,response,cookieName,cookieValue,cookieMaxAge,null, null); } /** * 设置cookie,不指定httpOnly属性 */ public static final void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, Integer cookieMaxAge, String encodeString) { setCookie(request,response,cookieName,cookieValue,cookieMaxAge,encodeString, null); } /** * 设置Cookie的值,并使其在指定时间内生效 * * @param cookieMaxAge * cookie生效的最大秒数 */ public static final void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, Integer cookieMaxAge, String encodeString, Boolean httpOnly) { try { if(StringUtils.isBlank(encodeString)) { encodeString = "utf-8"; } if (cookieValue == null) { cookieValue = ""; } else { cookieValue = URLEncoder.encode(cookieValue, encodeString); } Cookie cookie = new Cookie(cookieName, cookieValue); if (cookieMaxAge != null && cookieMaxAge > 0) cookie.setMaxAge(cookieMaxAge); if (null != request)// 设置域名的cookie cookie.setDomain(getDomainName(request)); cookie.setPath("/"); if(httpOnly != null) { cookie.setHttpOnly(httpOnly); } 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; } }
这个时候,我们的令牌已经下发了,但是我们只给了令牌30分钟的生存时间,每当用户有操作,也就是每当用户查询用户个人信息的话,就给他重新生成一个token。
/** * 用户操作,需要刷新token * @param token * @param request * @param response * @return */ @GetMapping("verify") public ResponseEntity<UserInfo> getUserInfo( @CookieValue("LY_TOKEN") String token, HttpServletRequest request, HttpServletResponse response ){ try { UserInfo userInfo = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey()); //只要用户有这个请求,就给用户一个新的token,防止过期 String newToken = JwtUtils.generateToken(userInfo, jwtProperties.getPrivateKey(), jwtProperties.getExpire()); //这里的true谁不允许js操作的意思 CookieUtils.setCookie(request,response,jwtProperties.getCookieName(), newToken,jwtProperties.getCookieMaxAge(),null,true); return ResponseEntity.status(HttpStatus.OK).body(userInfo); } catch (Exception e) { e.printStackTrace(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } }
为了保证某些服务是登陆的用户才能访问的,我们需要在网关设置拦截器
拦截器如下,因为是登陆的校验,肯定是一个前置的拦截器
@Component @EnableConfigurationProperties(value = {JwtProperties.class, FilterProperties.class}) public class LoginFilter extends ZuulFilter { private Logger logger = LoggerFactory.getLogger(LoginFilter.class); @Autowired private FilterProperties filterProperties; @Autowired private JwtProperties jwtProperties; private Boolean isAllowPath(String uri){ List<String> allowPaths = filterProperties.getAllowPaths(); boolean isFind = false; for (String allowPath : allowPaths) { if (uri.startsWith(allowPath)){ isFind = true; break; } } return isFind; } @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 5; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String requestURI = request.getRequestURI(); return !isAllowPath(requestURI); } @Override public Object run() throws ZuulException { //获取上下文 RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String token = CookieUtils.getCookieValue(request,jwtProperties.getCookieName()); try { JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey()); } catch (Exception e) { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(403); } return null; } }
当然,有些服务,我们不需要拦截,我们需要设置白名单,并把白名单设置到一个配置类中
@ConfigurationProperties(prefix = "ly.filter") public class FilterProperties { private List<String> allowPaths; public List<String> getAllowPaths() { return allowPaths; } public void setAllowPaths(List<String> allowPaths) { this.allowPaths = allowPaths; } }
这个地方,我们可以用集合接收
ly: jwt: pubKeyPath: H:/rsa/rsa.pub # 公钥地址 cookieName: LY_TOKEN # cookie的名称 filter: allowPaths: - /api/auth - /api/search - /api/user/register - /api/user/check - /api/user/send - /api/item
但是我们最后发现,cookien中并没有token
这是因为zuul内部的敏感头过滤,导致把我们的setCookie和cookie都过滤掉了,zuul中有个默认的过滤器,干了这个事情。
将zuul的敏感头设为null
因为如果我们没法setcookie的话,那么我们就没法向浏览器中写入cookie(响应头)
sensitive-headers: #敏感头设置为null
nginx和zuul在接收到请求的时候,都会对请求进行转发,转发到我们配置的ip地址上了
我们分别在nginx上和zull上配上我们的配置
nginx
zuul
add-host-header: true #携带host本身的请求头信息我们的目的是让nginx和zull不去修改我们的host
zuul的过滤器
我们让一个类继承zuulFilter就可以实现zuul的过滤器
@Component @EnableConfigurationProperties(value = {JwtProperties.class, FilterProperties.class}) public class LoginFilter extends ZuulFilter { private Logger logger = LoggerFactory.getLogger(LoginFilter.class); @Autowired private FilterProperties filterProperties; @Autowired private JwtProperties jwtProperties; private Boolean isAllowPath(String uri){ List<String> allowPaths = filterProperties.getAllowPaths(); boolean isFind = false; for (String allowPath : allowPaths) { if (uri.startsWith(allowPath)){ isFind = true; break; } } return isFind; } @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 5; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String requestURI = request.getRequestURI(); return !isAllowPath(requestURI); } @Override public Object run() throws ZuulException { //获取上下文 RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String token = CookieUtils.getCookieValue(request,jwtProperties.getCookieName()); try { JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey()); } catch (Exception e) { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(403); } return null; } }