乐优商城个人学习记录(第六部分)

此次跟随教程完成项目,主要目的是学习一些新的技术,掌握一些开发工具的使用。因此,并不会对事务逻辑进行细致记录。此篇博客仅用于博主本人日后搭建项目时的参考,并非教程

 

授权中心:

之前我们在做登陆功能时,会将已登录用户的信息保存在session中,通过校验session中的相关信息来判断用户是否登陆。但是在这次练习中,每个微服务均在不同的tomcat上运行,因此无法共享session信息;并且当我们的用户数量十分庞大时,所有的信息都保存在tomcat的session中显然是一个很不可取的做法。因此我们这里使用无状态登陆即服务端不保存用户的登录信息,而是让客户端自备信息,服务端只负责对这些信息进行校验。

无状态登陆流程:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登陆凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务对token进行解密,判断是否有效

我们采用JWT + RSA非对称加密来对token进行加密

JWT包含三部分数据:

  • Header:头部,通常头部有两部分信息:

    我们会对头部进行base64加密(可解密),得到第一部分数据

    • 声明类型,这里是JWT

    • 加密算法,自定义

  • Payload:载荷,就是有效数据,一般包含下面信息:

    这部分也会采用base64加密,得到第二部分数据

    • 用户身份信息(注意,这里因为采用base64加密,可解密,因此不要存放敏感信息)

    • 注册声明:如token的签发时间,过期时间,签发人等

  • Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性

乐优商城个人学习记录(第六部分)_第1张图片

使用RSA非对称加密的原因:

我们的私钥和加密算法只能保存在授权中心项目中,如果不采用RSA非对称加密,那就意味着只有授权中心能对token进行解析或加密。这样一来,我们每次调用微服务之前,都要先调用授权中心进行鉴权,这大大增加了授权中心的压力。如图:

乐优商城个人学习记录(第六部分)_第2张图片

采用RSA非对称加密后,私钥和加密算法仅保存在授权中心,授权中心负责使用私钥加密生成token。非对称加密中,使用私钥加密的数据,可使用公钥或私钥进行紧密,但是使用公钥进行加密的数据,只能使用私钥进行解密。我们可以将公钥颁发给我们信任的微服务,这样调用微服务时,微服务可直接解析token判断是否有效;但如果使用公钥进行加密,会导致其他持有公钥的微服务无法解析token,自然也就无法鉴权成功。这样一来,我们既确保了token的安全,又避免了授权中心被重复多次调用。如图:

乐优商城个人学习记录(第六部分)_第3张图片

下面来创建授权微服务,同样将其创建为一个聚合工程,下面包含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进行如下图所示的修改:

乐优商城个人学习记录(第六部分)_第4张图片

我们发现还是无法正确得到域名,这是因为我们的网关同样做了一次反向代理,我们在网关里面添加如下配置来解决:

zuul.add-host-header: true

zuul.sensitive-headers:

乐优商城个人学习记录(第六部分)_第5张图片

都更改后,我们发现还是没有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 values = operation.values();
        List carts = values.stream().map(o -> JsonUtils.parse(o.toString(), Cart.class)).collect(Collectors.toList());
        return carts;
    }

    public void updateNum(Long skuId, Integer num) {
        // 获取用户信息
        UserInfo user = UserInterceptor.getUserInfo();
        // 得到key和hashKey
        String key = KEY_PREFIX + user.getId();
        String hashKey = skuId.toString();
        // 绑定
        BoundHashOperations operations = template.boundHashOps(key);
        if(!operations.hasKey(hashKey)) {
            throw new LyException(ExceptionEnum.CART_NOT_FOUND);
        }
        String json = operations.get(hashKey).toString();
        Cart cart = JsonUtils.parse(json, Cart.class);
        cart.setNum(num);
        // 把数据写回redis
        operations.put(hashKey,JsonUtils.serialize(cart));

    }

    public void deleteCart(Long skuId) {
        // 获取用户信息
        UserInfo user = UserInterceptor.getUserInfo();
        // 得到key和hashKey
        String key = KEY_PREFIX + user.getId();
        template.opsForHash().delete(key,skuId.toString());
    }
}

购物车微服务完成

第六部分到此为止

你可能感兴趣的:(乐优商城个人学习记录(第六部分))