项目背景:
主流的大型电子商务系统虽然已经能解决大部分人的日常所需但是对于一些时间紧、额度低、跨域短或者基本不 跨域的业务还存在一定缺陷,比如常见的一些小商小贩,校园二手交易等。针对这种资金密度低,交易类型简单,交易快速 的业务需求,开发了本商城。
项目技术:
SpringBoot+MyBatis+HTML+CSS+JavaScript
项目链接
微信网络商城项目(点个star支持一下吧^_^)
项目模块介绍:
登录权限认证模块(单点登录模块),用户支付模块,统计用户信息模块。
项目的设计
表设计
表主要有user,product,category,order_product(商品和订单是多对多的关系),user_order(用户和订单是一对多的关系),order_evaluate(某订单的评价),address(保存用户的地址);
登录模块
任何系统都离不开登录,注册。登录模块如何设计就显得尤为重要。这里区别于传统的session机制,采用JWT来实现用户的登录。这里主要考虑的是传统的session机制,用户信息放在服务器内存,假如用户增多,消耗内存巨大,很容易出现OOM。好了,废话不多说,开始实现Jwt登录。
如果你还不了解什么是JWT的话,推荐下面这篇文章:
什么是JWT
接着我们在项目中使用JWT实现登录模块。
1.在项目中引入Json Web Token JJWT的依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2.使用JJWT实现JWT创建和验证
创建JwtUtils类:
package com.qf.utils;
import com.qf.enums.ApiEnum;
import com.qf.exception.GlobalException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.codec.binary.Base64;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
//密钥 -- 根据实际项目,这里可以做成配置
public static String KEY = "ED246B73392D0DCC97A9F399D24E56B2";
/**
* 由字符串生成加密key
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.decodeBase64(KEY);
return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}
/**
* 创建jwt
*
* @param id jwt唯一标识
* @param subject json格式的字符串
* @param ttlMillis 过期时间
* @Param claims
* @return
* @throws Exception
*/
public static String createJWT(String id, Map<String, Object> claims, String subject, long ttlMillis){
// 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
// 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
// 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
// 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
SecretKey key = generalKey();
// 下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
.setClaims(claims) // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setId(id) // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(now) // iat: jwt的签发时间
.setSubject(subject) // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥
// 设置过期时间
if (ttlMillis >= 0) {
long expMillis = nowMillis + (ttlMillis*1000);
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* 解密jwt
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) {
SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样
try{
return Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //设置签名的秘钥
.parseClaimsJws(jwt).getBody();
}catch (Exception e){
throw new GlobalException(ApiEnum.TOKEN_IS_ERROR);
}
}
/**
* 解析token 并转化map
* @param token
* @param keys
* @return
*/
public static Map<String,Object> getObject(String token,String... keys ){
Map<String,Object> map = new HashMap<>();
Claims claims = parseJWT(token);
for(String key : keys){
Object value = claims.get(key);
map.put(key,value);
}
return map;
}
}
3.用户登录逻辑
......//登录时判断用户名,密码,验证码是否正确相关逻辑
//上述正确,登录成功生成令牌返回给客户端,客户端保存好令牌避免再次登录
Map<String,Object> map = new HashMap<>();
map.put("user",user);
String token = JwtUtils.createJWT(user.getId().toString(), map, user.getUsername(), 6);
//构造返回结果
Map<String,Object> result = new HashMap<>();
result.put("user",user);
result.put("token",token);
//作废验证码
redisTemplate.delete(request.getUuid());
return result;
4.用户Token校验类JwtFilter
public class JwtFilter implements HandlerInterceptor {
private static final String noTokne = "/user/login,/user/verificationCode,/user/register,/doc.html," +
"/webjars/bycdao-ui/images/api.ico,/swagger-resources,/webjars/bycdao-ui/ace-editor/theme-eclipse.js," +
"/webjars/bycdao-ui/ace-editor/mode-json.js,/order/alipay_callback,/order/alipay_notify,/favicon.ico";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if(request.getMethod().equalsIgnoreCase("OPTIONS")||
request.getMethod().equalsIgnoreCase("GET")){
return true;
}
//排除不需要token的接口
String servletPath = request.getServletPath();
System.out.println("servletPath = " + servletPath);
for(String string : noTokne.split(",")){
if(servletPath.equals(string)){
return true;
}
}
//检测token头是否有效或者是否为空
String token = request.getHeader(JwtUtils.TOKEN_HEADER);
if(StringUtils.isBlank(token)){
throw new GlobalException(ApiEnum.HEANDER_IS_NULL);
}
Claims claims = JwtUtils.parseJWT(token);
LinkedHashMap user = claims.get("user", LinkedHashMap.class);
request.setAttribute("user",user);
return true;
}
}
5.注册请求拦截器
为了在每次请求服务器资源前都确保用户已登录(部分不需要登录即可访问的资源除外,如登录路径,用户注册路径,验证码路径),可以通过InterceptorRegistry把JwtFilter 注册到拦截器中。
@Configuration
public class MvcConfig implements WebMvcConfigurer {
//注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtFilter());
}
}
至此,就可以实现每次请求前的Token校验了。
接下来,让我们思考一下JWT有什么缺点???
1.安全性低。由于JWT默认将header,playload部分是json串用base64编码,意味着JWT是明文传输的,所以应避免直接传输敏感信息,比如密码,可以考虑将密码加密再进行传输(项目使用MD5加密存放(实际并不可取))。
另外,如果服务端密钥泄露,那么攻击者便可自主签发JWT,甚至直接以管理员的身份登录(篡改playload),后果不堪设想。所以要妥善保管密钥,并经常更换较强的key。
2.性能
如果JWT很大,使用JWT的http请求相比传统session的请求更慢。
支付模块
在讨论支付模块的实现之前,我们先熟悉下使用支付宝支付的流程:
推荐阅读:支付宝的支付流程
这里我们只关注商户服务端的工作:订单加签,支付结果验签,业务处理成功后告诉支付宝。
1.首先在项目中引入支付依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.124.ALL</version>
</dependency>
2.新建一个AlipayUtil类
public class AlipayUtil {
public static final String app_id = "2016091100484175";
//商户私钥
public static final String merchant_private_key = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCNaYRM+H0TmwLxM+B1AOM1uW15L4+4ozxtXGR6ozJvi8a/TqLM2sLvhBeJdMUnjMxRTHEuEuvvkYIpgEl2yZ2FertKR/vb3Leyiw92ff7qKe9oa7jlMUMZcthSPqB7tkq5ejVSgr4Yd6mmGQF04q5Eh+BRxWiiyAWWdthI9ZAHne6CnLyPDBOL9gqa6vnif7Fmcku+71E2lFgm6PPzQUUgUDhfW2WumqrpIkkJGJQmUFjtmVkSnbQbnNL1xgEJIbmBDUB9DYfpfaIl7EGyVWnFibu6nbNJ9kwjKIuXIsLXjDp+j0hBd1QLh0SDU1zgQ+xadwI6Shk2KQd2jdw8NjMFAgMBAAECggEAagKSJcmaqlnoiL2lJNF3g0zw3opjKb1ljW5X/P/ezXRxl/TxYrUUsc3Kf0dEjXOlr8SojCuvJ7djZo0/hYd7SRdK1FLGYnpCU9yfyOqHcS/iw9sNjvOxI6DOvkkosdVki0ssMiDFNT5vtY7FYsSUc9eQuHxPBskAN138oiVExg/n0WxHvzmAxieDnNRktZ+0zKoWaL0YDeSswia7I5o/QA/JMOiPK318y/pe/3iWdhvEuTATKit3iJBSeLsA7RIoGV1pw7QpntBeSh1UShvrTRl0c56BN+UrSR/QUYrKdauVime6jD9Fo1oZwYHTroAtT1dxn1lq2K5qqbmn9AMjwQKBgQDEshFYlnXusqG9PQbpFE8zK6cmjdIlxKDCPUG+HS0oRkygWQ/CX6yS98zp2H5a0uQMLpMFv2trx5e0Y2spu0SgN7VlALDXn4HhX5xstiZsb9ZJMjOSuJsIPZWYrHNAeS+0ea4Ndr8m0bQwCKHu1NT18jOLj+LNLqjjzxNgr/9L6QKBgQC4DGdzoF+z8i7ECwflhoplTMawlVFQXN2H2ZtiwsgNqodwIMxXSNHZqD4bOQZoepB6cvyMZr5Yxd0F7iC5Gk2/TQDtWkDgq47y1hIlYM+TMC4j200EHZAltw7R2mwqply3F9m1/jOmMYUkvRRCc1W5GeF6JxQyPxTS5gk5qnDovQKBgGNYgPRaglQ+evyGr0/YByyUNsd9SA/1YTDaGbN5Lw6xexBeC1ykUBim+iN+Skt7St1wRKfZh9sXI2Nj94NLZ5z4pjDOiYNOuB3p2ar60SthzTyJE41emkcuO7myGEAPNW4VKzj3qhJkRnsgURG9A5b5btlloir6DymItIPYQLzJAoGAAPgHv2MTveXDe0K3muy3Y8sgrKNMl0i13dY2bDGsTe4c4mk5yifW+vdYxFnrf7dNdWePjsmnrN31yOc1AuRxjlVAcP9togElMoAP/mRhE1xIkeXApQnmzVwGVvJ4aU0Q5eHZQo0BBpnyInxgU+05gUzyk+sKvyz31hhh6gzMpV0CgYAEhnTjXzuJNvUHYRKEZfyO4STG48ptjaj9O3hmDkEIDV63byhdwOJAHe4GaZMmmLr7JCy2TgIR2+ojFG6CHxzTaIeGOzEXHJ4FptLKIhh74pydwHnP0/+Y0KW/QbraFyfbevkEITWLZLwKqIvJu5RPCc4fLqRcNys0seQPzEsXnA==";
//支付宝公钥
public static final String alipay_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA827KiAW7uTnI9yWXdV0A2SharMga+U50Epfh5ZwnT8NIQzQXM47wjcon0TzCKkVSHrvxWuLvSLoBxmd5wmQhBnwf0Cvs6qpSroa0v9N6slyAQxeLryjK1tnDtxdboUosElicSzLvEeriSebaCH0Tfba8hl1+6CBiP4+2UaPW6Dub3zCO4XJ7UElpHNhx9b6yaZvpR/nq8n+NXzZRfWa4P5de4b/6U04u38bWZlmGV2LEn9DEdE+uoAOO6oOGzFsicQ7OgBNuUoyBNNpygE7CqTDh473qDw93VzSVAoS/pi2+6+vWdPUrjxjjgUKl1vc0kvmzmdQzypwQG1NKLnzx0wIDAQAB";
//异步post请求地址
//public static final String notify_url = "http://127.0.0.1:8080/order/alipay_notify";
public static final String notify_url = "http://localhost:8080/order/alipay_notify";
//同步get请求地址
public static final String return_url = "http://localhost:8080/order/alipay_callback";
//加密方法
public static final String sign_type = "RSA2";
//字符集编码
public static final String charset = "utf-8";
//支付宝网关
public static final String gatewayUrl = "https://openapi.alipaydev.com/gateway.do";
public static String connect(AlipayBean alipayBean) throws AlipayApiException {
//1、获得初始化的AlipayClient(客户端)
AlipayClient alipayClient = getAlipayClient();
//2、设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
//页面跳转同步通知页面路径
alipayRequest.setReturnUrl(return_url);
// 服务器异步通知页面路径
alipayRequest.setNotifyUrl(notify_url);
//封装参数
alipayRequest.setBizContent(JSON.toJSONString(alipayBean));
//3、请求支付宝进行付款,并获取支付结果
String result = alipayClient.pageExecute(alipayRequest).getBody();
//返回付款信息
return result;
}
//查询订单
public static AlipayTradeQueryResponse search(Map<String,String> map) throws AlipayApiException {
//1、获得初始化的AlipayClient
AlipayClient alipayClient = getAlipayClient();
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent(JSON.toJSONString(map));
AlipayTradeQueryResponse response = alipayClient.execute(request);
//返回付款信息
return response;
}
private static AlipayClient getAlipayClient() {
return new DefaultAlipayClient(
gatewayUrl,//支付宝网关
app_id,//appid
merchant_private_key,//商户私钥
"json",
charset,//字符编码格式
alipay_public_key,//支付宝公钥
sign_type//签名方式
);
}
}
3.orderController中pay方法调用alipayUtil类请求支付宝
orderController类
@Override
public String pay(String id,String addressId) throws AlipayApiException {
UserOrder userOrder = orderMapper.selectById(id);
redisTemplate.opsForValue().set("address-order"+userOrder.getId(),addressId,10,TimeUnit.MINUTES);
return payService.aliPay(new AlipayBean().setBody(addressId)
.setOut_trade_no(userOrder.getId().toString())
.setTotal_amount(new StringBuffer(userOrder.getOrderPrice().toString()))
.setSubject(UUID.randomUUID().toString()));
}
payService.aliPay(xx)实际上调用的就是AlipayUtil的connect方法,请求支付宝进行付款,并获取支付结果。
public static String connect(AlipayBean alipayBean) throws AlipayApiException {
//1、获得初始化的AlipayClient(客户端)
AlipayClient alipayClient = getAlipayClient();
//2、设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
//页面跳转同步通知页面路径
alipayRequest.setReturnUrl(return_url);
// 服务器异步通知页面路径
alipayRequest.setNotifyUrl(notify_url);
//封装参数
alipayRequest.setBizContent(JSON.toJSONString(alipayBean));
//3、请求支付宝进行付款,并获取支付结果
String result = alipayClient.pageExecute(alipayRequest).getBody();
//返回付款信息
return result;
}
注:这里的result是支付宝返回的包含支付二维码的页面;
4.支付成功后,支付宝通知服务端已经支付成功(通过之前设置的return_url),服务端需要对支付结果进行验签,支付成功则修改订单状态为待收货,然后进行页面跳转告诉用户已经支付成功。
@RequestMapping(value = "/alipay_callback",method = RequestMethod.GET)
@ResponseBody
public ApiResult callback(HttpServletRequest request,HttpServletResponse response) {
Map<String, String> params = convertRequestParamsToMap(request); // 将异步通知中收到的待验证所有参数都存放到map中
try {
// 调用SDK验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayUtil.alipay_public_key,
AlipayUtil.charset, AlipayUtil.sign_type);
if (signVerified) {
AlipayNotifyParam alipayNotifyParam = buildAlipayNotifyParam(params);
//修改订单状态
orderService.changeOrder(OrderEnum.C,alipayNotifyParam.getOutTradeNo(),null);
//response.sendRedirect("http://150.158.199.52:8848/item-shop/page/orderdesc.html?id="+alipayNotifyParam.getOutTradeNo());
response.sendRedirect("http://127.0.0.1:8848/item-shop/page/orderdesc.html?id="+alipayNotifyParam.getOutTradeNo());
}
} catch (AlipayApiException e) {
return R.fail();
} catch (IOException e) {
e.printStackTrace();
}
return R.fail();
}