JWT 实现登录
我们首先回顾下过去登录的方式,用户页面中输入账号和密码,点击提交发送到 controller 类中,controller 类接收账号和密码,并且调用业务和数据层去判断账号和密码是否正确,如果错误,返回到登录页面,如果正确,将用户信息保存到 session 中并跳转到主页面。
这里会发现,我们是通过 session 来记录用户登录状态,跟踪用户信息,那么就要在这里提一提 session 登录的缺陷:
1.session 是将客户端数据储存在服务器的内存,当客户端的数据过多,连接较多,服务器的内存开销大。
2.session 的数据储存在某台服务器,在分布式的项目中无法做到共享。
3.前后端分离的项目中共享 session 比较困难。
4.jwt 不需要在服务端去保留用户的认证信息或者会话信息。
1.什么是 JWT
jwt 全称是 json web token。是由用户以用户名、密码登录,服务端验证后,会生成一个 token,返回给客户端,客户端在下次访问的过程中携带这个 token,服务端责每次验证这个token。
2.JWT 的构成
jwt 由三部分组成,每一部分之间用符号"."进行分割,整体可以看做是一个长字符串。一个经典的jwt的样子:xxx.xxx.xxx。
1.Header 头部
头部由两部分组成:第一部分是声明类型,在 jwt 中声明类型就 jwt,第二部分是声明加密的算法,加密算法通常使用 HMAC|SHA256。一个经典的头部:
{
'typ': 'JWT', // 'typ':'声明类型'
'alg': 'HS256' // 'alg':'声明的加密算法'
}
2.Payload 载体、载荷
这一部分是jwt的主体部分,这一部分也是json对象,可以包含需要传递的数据,其中jwt指定了七个默认的字段选择,这七个字段是推荐但是不强制使用的:
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于识别该 JWT
除了上述的七个默认字段之外,还可以自定义字段,通常我们说 JWT 用于用户登陆,就可以在这个地方放置用户的id和用户名。下面这个json对象是一个 jwt 的 Payload 部分:
{
"sub": "一个演示",
"nickname": "zgh",
"id": "001"
}
这里注意虽然可以放自定的信息,但是不要存放一些敏感信息,除非是加密过的,因为这里的信息可能会被截获。
3.signature 签证
这部分是对前两部分进行base64编码在进行加密,这个加密的方式使用的是jwt的头部声明中的加密方式,在加上一个密码(secret)组成的,secret 通常是一个随机的字符串,这个 secret是服务器特有的,不能够让其他人知道。这部分的组成公式是:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
3.JWT 的优点
1.json形式,而json非常通用性可以让它在很多地方使用
2.jwt所占字节很小,便于传输信息
3.需要服务器保存信息,易于扩展
4.使用 JWT 登录流程
1.第一次登录的时候,前端调后端的登陆接口,发送帐号和密码。
2.后端收到请求,验证帐号和密码,验证成功,就给前端返回一个 jwt。
3.前端拿到 jwt,将 jwt 存储到 localStroage 或 header 中,并跳转到页面。
4.前端每次跳转页面,就判断 localStroage 中有无 jwt ,没有就跳转到登录页面,有则跳转到对应页面。
5.每次调后端接口,都要在请求头中加 jwt。
6.后端判断请求头中有无 jwt,有 jwt,就拿到 jwt 并验证 jwt,验证成功就返回数据,验证失败(例如:jwt 过期)就返回401,请求头中没有 jwt 也返回401。
7.如果前端拿到状态码为401,就清除 jwt 信息并跳转到登录页面。
5.搭建项目-JWT登录
搭建SpringBoot目录:
所需依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- jaxb依赖包 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/zgh
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 192.168.65.3
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
util包
JsonResult
package zgh.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @ClassName JsonResult
* @Description TODO
* @Author
* @Date 2023/8/12 9:53
* @Version 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JsonResult<T> implements Serializable {
private Boolean success;
private String error;
private Integer code;
private T data;
}
JwtConfig
package zgh.util;
import com.alibaba.druid.util.StringUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import zgh.bean.User;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* @ClassName JwtConfig
* @Description TODO
* @Author
* @Date 2023/8/12 9:54
* @Version 1.0
*/
public class JwtConfig {
//常量
public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
public static final String APP_SECRET = "1234"; //秘钥,加盐
// @param id 当前用户ID
// @param issuer 该JWT的签发者,是否使用是可选的
// @param subject 该JWT所面向的用户,是否使用是可选的
// @param ttlMillis 什么时候过期,这里是一个Unix时间戳,是否使用是可选的
// @param audience 接收该JWT的一方,是否使用是可选的
// 生成token字符串的方法
public static String getJwtToken(User user) {
String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT") //头部信息
.setHeaderParam("alg", "HS256") //头部信息
//下面这部分是payload部分
// 设置默认标签
.setSubject("root") //设置jwt所面向的用户
.setIssuedAt(new Date()) //设置签证生效的时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) //设置签证失效的时间
//自定义的信息,这里存储id和姓名信息
.claim("id", user.getId()) //设置token主体部分 ,存储用户信息
.claim("nickname", user.getNickname())
//下面是第三部分
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
// 生成的字符串就是jwt信息,这个通常要返回出去
return JwtToken;
}
/**
* 判断token是否存在与有效
* 直接判断字符串形式的jwt字符串
*
* @param jwtToken
* @return
*/
public static boolean checkToken(String jwtToken) {
if (StringUtils.isEmpty(jwtToken)) return false;
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
return false;
}
return true;
}
/**
* 根据token字符串获取会员id
* 这个方法也直接从http的请求中获取id的
*
* @param request
* @return
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if (StringUtils.isEmpty(jwtToken)) return "";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return claims.get("id").toString();
}
/**
* 解析JWT
* @param jwt
* @return
*/
public static Claims parseJWT(String jwt) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwt).getBody();
return claims;
}
}
ResultTool
package zgh.util;
/**
* @ClassName ResultTool
* @Description TODO
* @Author
* @Date 2023/8/12 9:55
* @Version 1.0
*/
public class ResultTool {
public static JsonResult success() {
return new JsonResult(true, null, 200, null);
}
public static JsonResult success(Object data) {
return new JsonResult(true, null, 200, data);
}
public static JsonResult fail(String msg) {
return new JsonResult(false, msg, 500, null);
}
}
SpringBootJwtApplication
@SpringBootApplication
public class SpringBootJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootJwtApplication.class, args);
}
}
bean包
User
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private Integer id;
private String name;
private String password;
private String nickname;
private String gender;
private String birthday;
}
controller包
UserController
//跨域
@CrossOrigin
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService service;
@PostMapping("/login")
public JsonResult login(User user) {
return service.login(user);
}
@GetMapping("/is")
public JsonResult is() {
return ResultTool.success();
}
}
service包
UserService
public interface UserService extends IService<User> {
JsonResult login(User user);
}
UserServiceImpl
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private UserMapper mapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public JsonResult login(User user) {
// 验证账号是否存在
User user1 = login(user.getName());
//验证密码是否正确
if (!user1.getPassword().equals(user.getPassword())) {
throw new UserPasswordErrorException("密码错误!");
}
//账号面膜正确
//生成jwt字符串
String token = JwtConfig.getJwtToken(user1);
//保存到redis中去
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set("TOKEN:" + user1.getId(), token, 1, TimeUnit.DAYS);
//返回给前端页面
return ResultTool.success(token);
}
private User login(String username) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", username);
User user = mapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("用户名没有找到!");
}
return user;
}
}
handler包
GlobalExceptionHandler
//全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UsernameNotFoundException.class)
public JsonResult usernameNotFoundException(RuntimeException e) {
return ResultTool.fail(e.getMessage());
}
@ExceptionHandler(UserPasswordErrorException.class)
public JsonResult userPasswordErrorException(RuntimeException e) {
return ResultTool.fail(e.getMessage());
}
}
UsernameNotFoundException
public class UsernameNotFoundException extends RuntimeException{
public UsernameNotFoundException(String message) {
super(message);
}
}
UserPasswordErrorException
public class UserPasswordErrorException extends RuntimeException{
public UserPasswordErrorException(String message) {
super(message);
}
}
mapper包
UserMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
启动类
@SpringBootApplication
public class SpringBootJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootJwtApplication.class, args);
}
}
测试
1.用户名正确,密码错误
2.用户名正确,密码正确
3.https://jwt.io/,进入该网站可查看token
4.查看redis中
前端部分
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="app">
<span style="color:red">{{error}}</span><br />
账号:<input type="text" v-model="name" /><br />
密码:<input type="password" v-model="password" /><br />
<button @click="login">登陆</button>
</div>
</body>
</html>
<script src="./axios.min.js"></script>
<script src="./vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
name: '',
password: '',
error: ''
}
},
methods: {
login() {
let _this = this
let data = new URLSearchParams()
data.append('name', this.name)
data.append('password', this.password)
axios({
url: 'http://localhost:8080/user/login',
data: data,
method: 'post'
}).then((response) => {
if (response.data.success) {
window.localStorage.setItem('token', response.data.data)
// 跳转
_this.error = '登陆成功'
} else {
_this.error = response.data.error
}
})
}
}
})
</script>
1.用户名错误,密码错误
2.用户名和密码都正确
3.有token字符串
4.查看redis中的和浏览器是否匹配,判断登录成功与否。
拦截器的跨域问题
起初解决 Springboot 跨域问题的方法是直接在 Controller 上添加 @CrossOrigin 注解,实现了前后端分离中的跨域请求。但随着业务代码的编写,做了 token 会话保持的检验,添加了拦截器后,再次出现了跨域问题。
很纳闷,按理说后台已经允许了跨域请求,之前的测试也证明了这一点,那为什么又突然出现了跨域拦截问题呢?
首先,在登录拦截器中作了校验,对于需要登录后才能访问的接口,如果请求头中没有携带 token,则是非法请求,直接返回404码。然后由于一直有对拦截到的请求中的请求头中的 token 做打印,所以出现问题的时候,控制台打印的 token 值为null,打开浏览器的开发者工具查看请求头也发现没有携带成功。而在没有添加拦截器之前上述问题都是不存在的,都是正常的,所以都不用考虑是前端问题,问题肯定出在后端,准确的说是拦截器。
那么为什么浏览器不能成功发送 token 呢?根据线索在更详细的查看了 CROS 的介绍后发现,原来 CROS 复杂请求时会首先发送一个 OPTIONS 请求做嗅探,来测试服务器是否支持本次请求,请求成功后才会发送真实的请求;而 OPTIONS 请求不会携带任何数据,导致这个请求不符合我们拦截器的校验规则被拦截了,直接返回了状态码,响应头中也没携带解决跨域需要的头部信息,进而出现了跨域问题。所以在浏览器调试工具中会发现该次请求没有携带 token,后端控制台打印 token 也为 null。
其次,就算这样,为什么会发生在添加跨域相关头部信息前就提前结束请求的这种情况呢?难道自定义的拦截器优先于 @CrossOrigin 注解执行?
通过查阅资料,解析 @CrossOrigin 注解的源码得知,如果 Controller 在类上标了 @CrossOrigin 或在方法上标了 @CrossOrigin 注解,则 Spring 在记录 mapper 映射时会记录对应跨域请求映射,将结果返回到 AbstractHandlerMethodMapping,当一个跨域请求过来时,Spring 在获取 handler 时会判断这个请求是否是一个跨域请求,如果是,则会返回一个可以处理跨域的 handler。
总结起来 @CrossOrigin 的原理相当于和 Handler 进行强绑定。
于是现在的问题又到了:Handler 和拦截器的执行顺序?
DispatchServlet.doDispatch() 方法是 SpringMVC 的核心入口方法,经过分析发现所有的拦截器的 preHandle() 方法的执行都在实际 handler 的方法之前,其中任意拦截器返回 false 都会跳过后续所有处理过程。而 SpringMVC 对预检请求的处理则在 PreFlightHandler.handleRequest() 中处理,在整个处理链条中出于后置位。由于预检请求中不带数据,因此先被权限拦截器拦截了。
所以每次获取不到 token 的请求都是 OPTIONS 请求,那么解决的方法就很明了了:把所有的OPTIONS请求统统放行。
//拦截器取到请求先进行判断,如果是OPTIONS请求,则放行
if("OPTIONS".equals(httpServletRequest.getMethod().toUpperCase())) {
System.out.println("Method:OPTIONS");
return true;
}
还有另一种方式,就是通过过滤器来处理跨域请求,众所周知,过滤器在拦截器之前执行,能够有效避免拦截处理的情况发生。
@Slf4j
@WebFilter("/*")
// 跨域过滤器
public class CrosFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("过滤处理过滤器启动了...");
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
log.info("跨域处理过滤器");
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
if (StringUtils.isEmpty(request.getHeader("Origin"))) {
response.setHeader("Access-Control-Allow-Origin", "*");
} else {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
}
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
6.jwt验证是否登录
注:删除controller
filter包
CorsFilter
@WebFilter("/*")
@Component
public class CorsFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "*");
chain.doFilter(request, response);
}
public void init(FilterConfig filterConfig) {
}
public void destroy() {
前端部分
用户一来,验证是否登录,需要一个首页
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="app">
Hello,world!
</div>
</body>
</html>
<script src="./axios.min.js"></script>
<script src="./vue.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {}
},
methods: {
checkIsLogin() {
//console.log(window.localStorage.getItem('token'))
axios.get('http://localhost:8080/user/is', {
headers: {
token: window.localStorage.getItem('token')
}
}).then((response) => {
if (!response.data.success) {
location.href = '3.html'
}
console.log(response.data)
})
}
},
created() {
this.checkIsLogin()
}
})
</script>
controller包
UserController
@GetMapping("/is")
public JsonResult is() {
return ResultTool.success();
}
资源能不能访问,验证是否登录,通过拦截器完成,在访问请求之前,拦截去执行,给出成功与失败结果
interceptor包
CheckIsLoginInterceptor
//拦截器
@Slf4j
@Component
public class CheckIsLoginInterceptor implements HandlerInterceptor {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 校验用户是否登陆
log.info("开始校验用户是否登陆");
// 前端发送过来的
String token = request.getHeader("token");
log.info("用户发送过来的token:{}", token);
// 校验token是否合法(是否携带,是否篡改)
boolean flag = JwtConfig.checkToken(token);
log.info("flag:{}", flag);
if (!flag) {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
// throw new UserNameNotFoundException("请登录后访问");
out.println(JSONArray.toJSONString(ResultTool.fail("请登录后访问")));
return false;
}
// 获取到id
Claims claims = JwtConfig.parseJWT(token);
String id = claims.get("id").toString();
log.info("获取用户的id:{}", id);
// 获取到redis中的token
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
String redisToken = valueOperations.get("TOKEN:" + id);
log.info("redis中存放的token:{}", redisToken);
// 校验redis的token和发送过来的token
if (!token.equals(redisToken)) {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
//throw new UserNameNotFoundException("请登录后访问");
out.println(JSONArray.toJSONString(ResultTool.fail("请登录后访问")));
return false;
}
log.info("用户是登陆状态");
return true;
}
}
handler包
UserNotLoginException
public class UserNotLoginException extends RuntimeException{
public UserNotLoginException(String message) {
super(message);
}
}
GlobalExceptionHandler
@ExceptionHandler(UserNotLoginException.class)
public JsonResult userNotLoginException(RuntimeException e) {
return ResultTool.fail(e.getMessage());
}
配置拦截器
config包
SpringMVCConfig
//配置拦截器
@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {
@Resource
private CheckIsLoginInterceptor checkIsLoginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkIsLoginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login");
}
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
启动并查看,拦截器设置成功
⽀付宝移动
技术点
1. ⽀付宝沙箱⼯具
2. ⽀付宝密钥⽣成⼯具
3. SpringBoot技术
4. Maven技术
5. Idea开发⼯具
1.前期准备
1.1 ⽀付宝沙箱⼯具
注册
⽀付宝开放平台申请⽀付接⼝:登录 - 支付宝 (alipay.com)
使⽤⾃⼰的⽀付宝账号扫码登陆。
沙箱应⽤
APPID:沙箱唯⼀识别码
沙箱账号
这些账号都是虚拟账号,我们在⽀付时使⽤的账号信息。
卖家账号
虚拟的收款⼈信息。
买家账号
虚拟的付款⼈信息,可以随时充值和提现。
沙箱⼯具
体验⼿机扫码⽀付的效果
1.2 安装⽀付宝密钥⽣成⼯具
下载
从⽀付宝开放平台下载 [生成密钥 - 支付宝文档中心 (alipay.com)],下载指定版本的⼯具就⾏
安装
⼀路下⼀步,注意安装时不能存放到空格路径。
打开软件
1.3 沙箱账号绑定密钥
2.创建项⽬
创建 SpringBoot 项⽬
搭建如图项目
引⼊依赖
<dependencies>
<!--阿里支付的依赖-->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>3.1.0</version>
</dependency>
<!--引入转换JSON格式依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.10</version>
</dependency>
</dependencies>
application.yml
##appid
appId: 9021000125620852
##私钥
privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCeMMXLRvCmcA0raMAud32dECIlk8U9XLI257vWTOCFHB7ujYPRb4F+1k3ttivCm0o8UFTZ3fWc37rM8ErKloeKumHj1/+0H/VgD6Ag/DNALA9AkHt6WPWIdYWfmTBCiujPkAeNbKfOETybprv3ADEHxzZHmzx9pmYIoPdnrfPtcRXM0bhKZFFGEVXWjpyCApwBwmrlMUc0RR0VFzurqRYE978aIapM3Q1eJvnlfwVzBYnxf4r25mtLsSOSXvVS+qNihwlNhAEntWVEn/I72erv20l6OxWf1ER1cTXClcN6ibECh2U+SBUtaVRq8ODE1zYLmm8jEtP6G1ZeBX6IpPOnAgMBAAECggEAcb5aCm8IzHQXVBYu0XqDrLKUCvb0xMlpL0dy7YU7jxqINzk0BhyqPRw0zm7FilmEiFeumzUYzOKl+4PwEzknXp3jkyOVrduo4Jh7qBwPcd38XY1F69QZQDRYj0hjxgUBn3VNqOfuxKHqNXUvBExz9MbOBbDeTu6dwSiUnE0c7qXPuWY7EEMNvWoNL0rWBbQkH4PrlTIUQPsUJcV81AyrwRowFNe2sANIkMaDw554CMnfCZ4x6SUXCbQtkytS5S6xTJX3zwKVZ6lIKD3D0lHKJhY5c2RRL0lQaQD/MJdakhZrApNakYV7ie1IG4iWR4MLJOUcqXUdrCLGKwLfO4u72QKBgQD7xgQPuaLiqTbwdw/6gus90TLhPZI3QFjHghOGu4e/uM1tgAtD0MxwuQPi190DlCapkBFEhlZHIcpDt+7KFkQ/n3bAvYEHePMAPegAC/d1IJxDXi4kz549FVjdAdcRnlrgBOke7fV6S482eW6XAijady95qbH8Ggcdp3E2L0HlywKBgQCg2Jaq0TqciCf0LuDXauk5dNTzC+Gzk6O6HFHcupQV2heR5BosUF2DNDQ0iQLbIoMnT6/IrcWlV0jYIbV43Fcs7INIqRNR2i8KwWYrPVqnWv9gzadyJMyZpAcGyywqgTKpgFBF0fBHu/SKeKV0TXRyu9oCRpGHiWp2V3h7k4QOFQKBgFmlZPbQa133UFeInUjearJlFY+7o59GqxXGi/tSNICgZYzSpbAs3U5ZojeYEtreWnHPmUZj6r6DGojIKh8MlJpuxhLUpuepOiTg5gV2PVMYHGukUhvLTRWEz1JKAHEGiGbxeKwJoHk+BC0qSaU9IJzBCUdxk60m+1ekshvP8c/HAoGAU+jJqPEfv3s0RKmT+C044BV35hcjtnfl6PhPKHRZPpEYzK4PjWCbeA/q0CFN0R3PB9oFXQ5yVlATm1Tyg8uG2tPDpUs23fORqO29q/8E5NuO8GQ4304dQmWUmNGzB7WAxXY/6jycOf/ukbJgtiyV/CjNkXRZBTkFxhjZrLYHJQUCgYEAmI+9OSp4EqAn+8xKk4MFZgwuM5B9tX/yCjtyfQz0SnXy4vus18gXhI1/OIdtQtUaYz2+CuzdB8nLGRDGgkyXxo7j4mGahIfQ9PYXHqOPxW9fS5mxyCD23JKjgJl5HLLCGlXwvNamWsho7xfzUrQxbj7nm8Qusk2SKPzt1z96eC4=
##⽀付宝公钥
publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnjDFy0bwpnANK2jALnd9nRAiJZPFPVyyNue71kzghRwe7o2D0W+BftZN7bYrwptKPFBU2d31nN+6zPBKypaHirph49f/tB/1YA+gIPwzQCwPQJB7elj1iHWFn5kwQoroz5AHjWynzhE8m6a79wAxB8c2R5s8faZmCKD3Z63z7XEVzNG4SmRRRhFV1o6cggKcAcJq5TFHNEUdFRc7q6kWBPe/GiGqTN0NXib55X8FcwWJ8X+K9uZrS7Ejkl71UvqjYocJTYQBJ7VlRJ/yO9nq79tJejsVn9REdXE1wpXDeomxAodlPkgVLWlUavDgxNc2C5pvIxLT+htWXgV+iKTzpwIDAQAB
##⽀付成功后跳转的路径
notifyUrl: http://localhost:8080/alipay/success
returnUrl: http://localhost:8080/alipay/success
##签名⽅式
signType: RSA2
##编码集
charset: utf-8
##⽀付宝⽹关,在沙箱中获取
gatewayUrl: https://openapi-sandbox.dl.alipaydev.com/gateway.do
封装类
封装类模拟⽀付时的⼀些参数,⽐如说:订单号、名称、⾦额、描述等
bean——>Alipay
@Data
public class Alipay implements Serializable {
// 描述一个订单号
private String out_trade_no;
// 订单名称
private String subject;
// 订单付款金额
private String total_amount;
// 订单的描述
private String body;
// PC网页支付必传参数
private String product_code = "FAST_INSTANT_TRADE_PAY";
}
static–>index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--表单,通过表单将一个值传递给后端-->
<form action="alipay/pay" method="post">
订单号:<input type="text" name="out_trade_no"/><br/>
订单名称:<input type="text" name="subject"/><br/>
金额:<input type="number" name="total_amount"/><br/>
描述:<input type="text" name="body"/><br/>
<button>付款</button>
</form>
</body>
</html>
控制器
接收⽤户的请求,跳⽤⽀付宝接⼝实现付款,成功后返回结果。
controller–>AlipayController
package zgh.controller;
import com.alibaba.fastjson.JSONArray;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import zgh.bean.Alipay;
/**
* @ClassName AlipayController
* @Description TODO
* @Author
* @Date 2023/8/12 16:18
* @Version 1.0
*/
// 接收用户付款信息,调用支付宝接口,成功返回
@RestController // 产生这个类的对象
@RequestMapping("/alipay")
public class AlipayController {
@Value("${appId}")
private String appId;
@Value("${privateKey}")
private String privateKey;
@Value("${publicKey}")
private String publicKey;
@Value("${notifyUrl}")
private String notifyUrl;
@Value("${returnUrl}")
private String returnUrl;
@Value("${signType}")
private String signType;
@Value("${charset}")
private String charset;
@Value("${gatewayUrl}")
private String gatewayUrl;
@RequestMapping("/pay")
public String pay(Alipay alipayBean) throws AlipayApiException {
// 产生Alipay客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl, appId, privateKey, "json", charset, publicKey, signType);
// 调用支付宝接口
AlipayTradePagePayRequest alipayTradeAppPayRequest = new AlipayTradePagePayRequest();
System.out.println("进入了这里:" + alipayBean);
// 设置付款成功后应跳转的路径
alipayTradeAppPayRequest.setNotifyUrl(notifyUrl);
alipayTradeAppPayRequest.setReturnUrl(returnUrl);
// 设置支付宝的各项参数
String json = JSONArray.toJSONString(alipayBean);
alipayTradeAppPayRequest.setBizContent(json);
System.out.println("支付的参数是:" + json);
System.out.println(JSONArray.toJSONString(alipayTradeAppPayRequest));
// 生成最终的订单
return alipayClient.pageExecute(alipayTradeAppPayRequest).getBody();
}
@RequestMapping("/success")
public String success(){
return "支付成功";
}
}
启动类
SpringBootAlipayApplication
@SpringBootApplication
public class SpringBootAlipayApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootAlipayApplication.class, args);
}
}
运行结果
启动并清空控制台,提交订单信息
填写登录信息
输入支付密码
确认支付
支付成功
后端控制台得到订单信息