0%

HTTP配置详解

@RestController

@RestController继承了@Controller@ResponseBody,该注解可同时发挥这两个注解的作用,@ResponseBody能将 @RequestMapping标注的方法的返回值以json的形式响应给浏览器

@SpringApplication

@SpringApplication继承了 @ComponentScan@SpringBootConfiguration和@EnableAutoConfiguration@ComponentScan的扫描规则是被 @SpringBootConfiguration注解直接或间接标注的类所在的包及其子包。

@EnableAutoConfiguration的作用则是根据项目引入的依赖自动配置相关技术所需的配置项(省去了原来的applicationContext-xxx.xml


PostMan接口调试工具的介绍和使用

PostMan是一款可以进行http接口调试和记录保存的http接口开发工具。他可以帮我们测试接口的功能性,同时我们可以将测试过的接口按模块划分保存下来,方便接口文档的编写和与测试人员的交接。

下载地址:https://www.getpostman.com/

测试接口

保存记录

创建分类文件夹(Collection)分类保存接口记录

一般按照接口所属的模块来创建Collection分类保存接口记录

将接口记录保存到Collection中

new tab填写url,点击save

url命名请求名称,并保存到Collection

更多PostMan功能可自行百度学习。


HTTP接口GET请求实战

@GetMapping

如果我们要限制某接口的提交方式,常见是这样:

1
2
3
4
5
6
7
8
private Map params = new HashMap();
@RequestMapping(path = "/getMapping/{city_id}/{user_id}",method = RequestMethod.GET)
public Object getParmas(@PathVariable("city_id") String cityId, @PathVariable("user_id") String userId) {
params.clear();
params.put("cityId",cityId);
params.put("userId", userId);
return params;
}

其实我们可以通过 @GetMapping简化此步:

1
2
3
4
5
6
7
@GetMapping("/getMapping/{city_id}/{user_id}")
public Object getParmas(@PathVariable("city_id") String cityId, @PathVariable("user_id") String userId) {
params.clear();
params.put("cityId",cityId);
params.put("userId", userId);
return params;
}

接口为什么用下划线而不用驼峰?因为有些语言不支持驼峰,为了确保接口的通用性,一般同一用小写+下划线

如果我们使用 PostMan 测试该接口:localhost:8080/getMapping/100/2

1
2
3
4
5
6
7
8
9
10
11
12
13
POST:
{
"timestamp": "2018-07-16T14:30:47.244+0000",
"status": 405,
"error": "Method Not Allowed",
"message": "Request method 'POST' not supported",
"path": "/getMapping/100/20"
}
GET:
{
"cityId": "100",
"userId": "20"
}

类似的还有 @PostMapping,@PutMapping,@DeleteMapping

@RequestParam

在请求发送的参数和controller中的方法参数之间做转换

1
2
3
4
5
6
@GetMapping("/test2")
public Object getParams(@RequestParam(defaultValue = "1", name = "page") String param1, String param2) {
params.put("param1", param1);
params.put("param2", param2);
return params;
}

你可以通过 defaultValue为参数附加一个默认值,防止空值可能带来的异常,可以通过 name将请求的参数名映射到方法形参的参数名上。

@RequestBody

编写 pojo

1
2
3
4
5
6
7
8
9
package top.zhenganwen.springbootweb.pojo;

import lombok.Data;

@Data
public class User {
private String username;
private String password;
}

关于 lombok插件可以简洁的实现一个pojo类,用法可参见https://blog.csdn.net/motui/article/details/79012846,IDEA需先安装此插件并引入依赖:

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>

如果pom文件报错:failed to read artifact descriptor for xx.jar则是由于相关依赖没有下载成功,需要更改 mavensettting.xml,将其中的 mirror改为访问正常且涵盖依赖较多的镜像,如我的是:

1
2
3
4
5
6
7
8
9
10
11
<mirrors>
<!-- mirror
Specifies a repository mirror site to use instead of a given repository. The repository that this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
-->
<mirror>
   <id>repo2</id>
   <mirrorOf>central</mirrorOf>
   <name>Human Readable Name for this Mirror.</name>
   <url>http://repo2.maven.org/maven2/</url>
</mirror>
</mirrors>

接口

1
2
3
4
5
6
@PostMapping("/test3")
public Object test3(@RequestBody User user) {
params.put("username", user.getUsername());
params.put("password", user.getPassword());
return params;
}

值得注意的是只有POST提交才可附加请求头而GET提交没有请求头。

这里通过 @RequestBody可设置请求头的 Content-Typeapplication/json

使用PostMan测试一下:

@RequestHeader获取头信息

有时我们会将系统同行令牌附加在头信息中,在请求中获取令牌进行验证

1
2
3
4
5
6
 @RequestMapping("/test4")
public Object test4(@RequestHeader("access_token") String accessToken,String id) {
params.put("accessToken", accessToken);
params.put("id", id);
return params;
}

HttpServletRequest自动注入

ControllerHttpServletRequest 类型的成员变量或方法形参会被自动注入当前的请求对象。

HTTP接口POST,PUT,DELETE请求实战

POST,PUT,DELETE提交性质与GET相同,只不过有这不同的语义:GET一般是查询,PUT一般是更新,DELETE一般是删除。


常用json框架介绍和Jackson返回结果处理

常用框架 阿里 fastjson,谷歌gson等

性能比较

JavaBean序列化为Json,性能:Jackson > FastJson > Gson > Json-lib (同个结构的条件下)

Jackson、FastJson、Gson类库各有优点,各有自己的专长。但无非就是空间换时间、时间换空间

@JsonIgnore指定不返回

  • Domain
1
2
3
4
5
6
@Data
@AllArgsConstructor
public class User {
private String username;
private String password;
}
  • Controller
1
2
3
4
5
@GetMapping("/test5")
public Object test5() {
User user = new User("tom", "123456"); //模拟查询用户
return user;
}
  • 接口测试
1
2
3
4
{
"username": "tom",
"password": "123456"
}

你会发现密码被带回到了前端,这并不利与用户安全。因此我们可以在此字段上标注 @JsonIgnore

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
public class User {
private String username;
@JsonIgnore
private String password;
}

再次测试你会发现此字段已被过滤了(javabean转json时忽略了该字段)。

@JsonFormat指定日期格式

1
2
3
4
5
6
7
8
9
@Data
@AllArgsConstructor
public class User {
private String username;
@JsonIgnore
private String password;
@JsonFormat(pattern="yyyy-MM-dd hh:mm:ss",locale="zh",timezone="GMT+8")
private Date createdTime;
}
1
2
3
4
{
"username": "tom",
"createdTime": "2018-07-17 12:13:21"
}

@JsonInclude空字段不返回

如果你不想返回的json中带有null值的字段:

1
2
3
4
5
6
7
private String username;
@JsonIgnore
private String password;
@JsonFormat(pattern="yyyy-MM-dd hh:mm:ss",locale="zh",timezone="GMT+8")
private Date createdTime;

private String phone;
1
2
3
4
5
@GetMapping("/test5")
public Object test5() {
User user = new User("tom", "123456",new Date(),null);
return user;
}
1
2
3
4
5
{
"username": "tom",
"createdTime": "2018-07-17 12:16:28",
"phone": null
}

你可以添加此标注

1
2
@JsonInclude(JsonInclude.Include.NON_NULL)
private String phone;

接口测试:

1
2
3
4
{
"username": "tom",
"createdTime": "2018-07-17 12:16:28"
}

@JsonProperty指定别名

有时攻击者会通过接口返回的json推敲数据库表字段和表结构以发现漏洞,这时我们需要伪装一下字段名称:

1
2
3
@JsonProperty("account")
@JsonInclude(JsonInclude.Include.NON_NULL)
private String phone;
1
2
3
4
5
@GetMapping("/test5")
public Object test5() {
User user = new User("tom", "123456",new Date(),"165435615");
return user;
}

接口测试:

1
2
3
4
5
{
"username": "tom",
"createdTime": "2018-07-17 01:52:37",
"account": "165435615"
}

SpringBoot2.x目录结构详解

maven目录结构

如果基于maven构建SpringBoot2.x,则 src/main/java用来存放代码, src/main/resources则用来存放静态资源和配置文件

resources下目录规则

  • static: 存放静态文件,比如 cssjsimage, (访问方式如 http://localhost:8080/js/main.js)
  • templates:存放静态页面jsp,html,tpl
  • config:存放配置文件
  • resources: 存放其他资源文件
  • public:存放可公开访问的文件

资源文件的查找顺序

Spring Boot 默认会挨个从META/resourcesMETAWEB-INF同级)> resources > static > public 里面找是否存在相应的资源,如果有则直接返回。

如果你的目录结构如下

  • src/main/resources
    • resources
      • resources.xml
    • static
      • static.html
    • public
      • public.js

访问如下路径均能找到:

Thymeleaf

如果在 resources/templates中创建 index.html,通过 localhost:8080/index.html是无法访问到的,此时我们需要引入 Thymeleaf依赖并通过 Controller映射:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
1
2
3
4
@RequestMapping("/test6")
public String test6() {
return "index";
}

通过 localhost:8080/test6便可访问到 resources/templates/index.html

自定义要加载的静态内容

基于SpringBoot2.x默认只会加载 META/resourcesresources/resourcesresources/publicresources/static中的文件,我们可能需要一些自定义的配置

如我们需要 resources/test/test.js能被直接访问到该怎么办?

可以在SpringBoot的全局配置文件resources/application.properties中添加如下配置:

1
spring.resources.static-locations = classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/test/

这样就可以保留约定俗成的,添加自定义的,加载顺序为以逗号分隔的顺序

访问 localhost:8080/test.js试试。

值得注意的是,高并发场景下servlet容器对静态资源并不友好,一般使用SpringBoot做前后端分离,前端的静态资源文件在页面中通过CDN加载,而不会放到SpringBoot中。


文件上传实战

静态页面直接访问

如果想要直接访问静态文件,需要将其放在springboot默认加载的文件夹下。

UploadController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package top.zhenganwen.springbootweb.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import top.zhenganwen.springbootweb.entity.JsonResult;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.UUID;

/**
* UploadController class
*
* @author zhenganwen
* @date 2018/7/17
*/
@RestController
@RequestMapping("upload")
public class UploadController {

@Autowired
private HttpServletRequest request;

@RequestMapping("/pic")
public JsonResult uploadPic(@RequestParam(name = "pic") MultipartFile file){

//校验文件是否为空,上传大小
...

String imgSavePath = ClassLoader.getSystemClassLoader().getResource("resources/images").getPath()+File.separator;

//获取文件扩展名
String originalFilename = file.getOriginalFilename();
String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);

//生成唯一文件名
String fileName = UUID.randomUUID() + "." + extName;


try {
//保存
//transferTo方法比自己用FileOutputStream便捷且更加高效
file.transferTo(new File(imgSavePath + fileName));
//返回图片路径
return new JsonResult(0, "success", "/images/" + fileName);
} catch (IOException e) {
e.printStackTrace();
return new JsonResult(-1, "fail to upload file");
}



}
}

JsonResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package top.zhenganwen.springbootweb.entity;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

@Data
@AllArgsConstructor
public class JsonResult implements Serializable {

/**
* 响应状态码
* 0表示操作成功
* -1表示操作失败
*/
private int code;
private String message;
private Object data;

public JsonResult(int code, String message) {
this.code = code;
this.message = message;
}

public JsonResult(int code, String message, Object data) {
this.code = code;
this.data = data;
}
}

upload.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传文件</title>
</head>
<body>
<form action="/upload/pic" method="post" enctype="multipart/form-data">
点击选择文件:<input type="file" name="pic">
<input type="submit" value="上传">
</form>
</body>
</html>

上传测试

1
2
3
4
5
{
"code":0,
"message": "success",
"data":"/images/f3a22bfd-3b7b-436f-82af-d46cfc4771ff.jpg"
}

访问图片:

1
localhost:8080/images/f3a22bfd-3b7b-436f-82af-d46cfc4771ff.jpg

上传大小的限制

SpringBoot默认将单个文件的上传大小限制为500KB左右(1048576 bytes),如果你需要自定义则需在有 @Configuration标注的类中添加如下配置:

1
2
3
4
5
6
7
8
9
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
//单个文件上传最大限制,字符串单位可选KB/B,数值单位为B
factory.setMaxFileSize("10240KB");
//上传数据总大小限制
factory.setMaxRequestSize("102400KB");
return factory.createMultipartConfig();
}

jar包方式运行web项目的文件上传和访问处理

jar包运行

当我们将maven+springboot构建的web项目 install之后,试图运行 target中的 jar文件时会报错找不到启动类

1
F:\IDEA_project\springboot-web\target>java -jar springboot-web-0.0.1-SNAPSHOT.jar

这时你需要添加一个项目构建的插件:

1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

你可以解压 springboot-web-0.0.1-SNAPSHOT.jar,查看其中的 META-INF/MANIFEST.MF

1
2
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: top.zhenganwen.springbootweb.SpringbootWebApplication

这里就为我们指定了应用程序的入口

jar包运行web应用上传图片到服务器

如果我们通过jar包运行web应用,那么我们上传的图片势必只能存放在jar包之外,那么如果通过该web应用访问图片呢?

首先将上传的图片保存到服务器的某个文件夹下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RequestMapping("/pic")
public JsonResult uploadPic(@RequestParam(name = "pic") MultipartFile file){

//String imgSavePath = ClassLoader.getSystemClassLoader().getResource("resources/images").getPath()+File.separator;

String imgSavePath = "C:/Users/zaw/Desktop/";

//获取文件扩展名
String originalFilename = file.getOriginalFilename();
String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);

//生成唯一文件名
String fileName = UUID.randomUUID() + "." + extName;


try {
//保存
//transferTo方法比自己用FileOutputStream便捷且更加高效
file.transferTo(new File(imgSavePath + fileName));
//返回图片路径
return new JsonResult(0, fileName);
} catch (IOException e) {
e.printStackTrace();
return new JsonResult(-1, "fail to upload file");
}

}

其次,我们需要在application.properties中将该文件夹添加到SpringBoot加载的路径中:

1
2
web.image-path=C:/Users/zaw/Desktop 	#web.image-path为变量名,名称任意
spring.resources.static-locations = classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:${web.image-path}

这样我们就能通过 http://localhost:8080/f09d2c03-d6f6-4618-99ea-3c58a2a193e0.jpg的形式访问到我们上传的图片了

文件服务器

在网站访问量较高时,我们需要将图片等静态资源文件与应用程序分离开来。常用的解决方案有FastDFS、阿里云ossnginx简单文件服务器等。

任务调度SpringTask

什么是任务调度

在企业级应用中,经常会制定一些“计划任务”,即在某个时间点做某件事情核心是以时间为关注点,即在一个特定的时间点,系统执行指定的一个操作。常见的任务调度框架有QuartzSpringTask等。

SpringTask入门小Demo

创建模块pinyougou-task-service,添加配置文件applicationContext-task.xml ,内容如下:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.2.xsd">
<context:component-scan base-package="com.pinyougou.task"/>
<task:annotation-driven/>
</beans>

创建包com.pinyougou.task

编写类:

1
2
3
4
5
6
7
8
9
10
@Component
public class SeckillTask {
/**
* 刷新秒杀商品
*/
@Scheduled(cron="* * * * * ?")
public void refreshSeckillGoods(){
System.out.println("执行了任务调度"+new Date());
}
}

执行后会看到控制台每秒都输出了当前时间,其中cron设置的为表达式,是执行的时间规则。

Cron表达式

Cron表达式格式

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:

(1)Seconds Minutes Hours DayofMonth Month DayofWeek Year

(2)Seconds Minutes Hours DayofMonth Month DayofWeek

每一个域可出现的字符如下:

  • Seconds:可出现”, - * /“四个字符,有效范围为0-59的整数
  • Minutes:可出现”, - * /“四个字符,有效范围为0-59的整数
  • Hours:可出现”, - * /“四个字符,有效范围为0-23的整数
  • DayofMonth:可出现”, - * / ? L W C“八个字符,有效范围为1-31的整数
  • Month:可出现”, - * /“四个字符,有效范围为1-12的整数或JAN-DEc
  • DayofWeek:可出现", - * / ? L C #“八个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推
  • Year:可出现”, - * /“四个字符,有效范围为1970-2099年

每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

(1)*:表示匹配该域的任意值,假如在Minutes域使用*, 即表示每分钟都会触发事件。

(2)?:只能用在DayofMonthDayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonthDayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样(表示放弃指定)。

(3)-:表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次

(4)/:表示在起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟开始触发一次,而25,45等分别触发一次.

(5),:表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。

(6)L:表示最后只能出现在DayofWeekDayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。

(7)W: 表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一 到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份

(8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五

(9)#:用于确定每个月第几个星期几只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三

Cron表达式例子

  • 0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
  • 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
  • 0 0 12 ? * WED 表示每个星期三中午12点
  • 0 0 12 * * ?“ 每天中午12点触发
  • 0 15 10 ? * *“ 每天上午10:15触发
  • 0 15 10 * * ?“ 每天上午10:15触发
  • 0 15 10 * * ? *“ 每天上午10:15触发
  • 0 15 10 * * ? 2005“ 2005年的每天上午10:15触发
  • 0 * 14 * * ?“ 在每天下午2点到下午2:59期间的每1分钟触发
  • 0 0/5 14 * * ?“ 在每天下午2点到下午2:55期间的每5分钟触发
  • 0 0/5 14,18 * * ?“ 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
  • 0 0-5 14 * * ?“ 在每天下午2点到下午2:05期间的每1分钟触发
  • 0 15 10 ? * MON-FRI“ 周一至周五的上午10:15触发
  • 0 15 10 15 * ?“ 每月15日上午10:15触发
  • 0 15 10 L * ?“ 每月最后一日的上午10:15触发
  • 0 15 10 ? * 6L“ 每月的最后一个星期五上午10:15触发
  • 0 15 10 ? * 6L 2002-2005“ 2002年至2005年的每月的最后一个星期五上午10:15触发
  • 0 15 10 ? * 6#3“ 每月的第三个星期五上午10:15触发

可直接上网搜在线Cron表达式生产器

秒杀商品列表的增量更新

每分钟执行查询秒杀商品表,将符合条件的记录并且缓存中不存在的秒杀商品存入缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 刷新秒杀商品
*/
@Scheduled(cron="0 * * * * ?")
public void refreshSeckillGoods(){
System.out.println("执行了任务调度"+new Date());
//查询所有的秒杀商品键集合
List ids = new ArrayList(redisTemplate.boundHashOps("seckillGoods").keys());
//查询正在秒杀的商品列表
TbSeckillGoodsExample example=new TbSeckillGoodsExample();
Criteria criteria = example.createCriteria();
criteria.andStatusEqualTo("1");//审核通过
criteria.andStockCountGreaterThan(0);//剩余库存大于0
criteria.andStartTimeLessThanOrEqualTo(new Date());//开始时间小于等于当前时间
criteria.andEndTimeGreaterThan(new Date());//结束时间大于当前时间
criteria.andIdNotIn(ids);//排除缓存中已经有的商品
List<TbSeckillGoods> seckillGoodsList= seckillGoodsMapper.selectByExample(example);
//装入缓存
for( TbSeckillGoods seckill:seckillGoodsList ){
redisTemplate.boundHashOps("seckillGoods").put(seckill.getId(), seckill);
}
System.out.println("将"+seckillGoodsList.size()+"条商品装入缓存");
}

过期秒杀商品的移除

每秒钟在缓存的秒杀上皮列表中查询过期的商品,发现过期则同步到数据库,并在缓存中移除该秒杀商品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 移除秒杀商品
*/
@Scheduled(cron="* * * * * ?")
public void removeSeckillGoods(){
System.out.println("移除秒杀商品任务在执行");
//扫描缓存中秒杀商品列表,发现过期的移除
List<TbSeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values();
for( TbSeckillGoods seckill:seckillGoodsList ){
if(seckill.getEndTime().getTime()<new Date().getTime() ){//如果结束日期小于当前日期,则表示过期
seckillGoodsMapper.updateByPrimaryKey(seckill);//向数据库保存记录
redisTemplate.boundHashOps("seckillGoods").delete(seckill.getId());//移除缓存数据
System.out.println("移除秒杀商品"+seckill.getId());
}
}
System.out.println("移除秒杀商品任务结束");
}

秒杀业务分析

需求分析

所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。

秒杀商品通常有两种限制:库存限制、时间限制。

需求:

(1)商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息

(2)运营商审核秒杀申请

(3)秒杀频道首页列出秒杀商品(进行中的)点击秒杀商品图片跳转到秒杀商品详细页。

(4)商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。

(5)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。

(6)当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存

数据库表分析

Tb_seckill_goods 秒杀商品表

Tb_seckill_order 秒杀订单表

秒杀实现思路

秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力!读取商品详细信息时运用缓存,当用户点击抢购时减少缓存中的库存数量,当库存数为0时或活动期结束时,同步到数据库。 产生的秒杀订单也不会立刻写到数据库中,而是先写到缓存,当用户付款成功后再写入数据库。

秒杀商品后台管理

商家后台

(1)秒杀商品列表

(2)秒杀商品申请

(3)秒杀订单查询

运营商后台

(1)待审核秒杀商品列表

(2)秒杀商品审核

(3)秒杀订单查询

秒杀频道首页

需求分析

秒杀频道首页,显示正在秒杀的商品(已经开始,未结束的商品)

后端代码

服务接口层

1
2
3
4
5
/**
* 返回当前正在参与秒杀的商品
* @return
*/
public List<TbSeckillGoods> findList();

服务实现层

1
2
3
4
5
6
7
8
9
10
@Override
public List<TbSeckillGoods> findList() {
TbSeckillGoodsExample example=new TbSeckillGoodsExample();
Criteria criteria = example.createCriteria();
criteria.andStatusEqualTo("1");//审核通过
criteria.andStockCountGreaterThan(0);//剩余库存大于0
criteria.andStartTimeLessThanOrEqualTo(new Date());//开始时间小于等于当前时间
criteria.andEndTimeGreaterThan(new Date());//结束时间大于当前时间
return seckillGoodsMapper.selectByExample(example);
}

控制层

1
2
3
4
5
6
7
8
/**
* 当前秒杀的商品
* @return
*/
@RequestMapping("/findList")
public List<TbSeckillGoods> findList(){
return seckillGoodsService.findList();
}

前端代码实现

服务层

1
2
3
4
5
6
7
//服务层
app.service('seckillGoodsService',function($http){
//读取列表数据绑定到表单中
this.findList=function(){
return $http.get('seckillGoods/findList.do');
}
});

控制层

1
2
3
4
5
6
7
8
9
10
11
//控制层 
app.controller('seckillGoodsController' ,function($scope,seckillGoodsService){
//读取列表数据绑定到表单中
$scope.findList=function(){
seckillGoodsService.findList().success(
function(response){
$scope.list=response;
}
);
}
});

缓存处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Autowired
private RedisTemplate redisTemplate;

@Override
public List<TbSeckillGoods> findList() {
//获取秒杀商品列表
List<TbSeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values();
if(seckillGoodsList==null || seckillGoodsList.size()==0){
TbSeckillGoodsExample example=new TbSeckillGoodsExample();
Criteria criteria = example.createCriteria();
criteria.andStatusEqualTo("1");//审核通过
criteria.andStockCountGreaterThan(0);//剩余库存大于0
criteria.andStartTimeLessThanOrEqualTo(new Date());//开始时间小于等于当前时间
criteria.andEndTimeGreaterThan(new Date());//结束时间大于当前时间
seckillGoodsList= seckillGoodsMapper.selectByExample(example);
//将商品列表装入缓存
System.out.println("将秒杀商品列表装入缓存");
for(TbSeckillGoods seckillGoods:seckillGoodsList){
redisTemplate.boundHashOps("seckillGoods").put(seckillGoods.getId(), seckillGoods);
}
}
return seckillGoodsList;
}

秒杀详细页

需求分析

商品详细页显示秒杀商品信息。

显示详细页信息

后端代码

1
2
3
4
/**
* 根据ID获取实体(从缓存中读取)
*/
public TbSeckillGoods findOneFromRedis(Long id);
1
2
3
4
@Override
public TbSeckillGoods findOneFromRedis(Long id) {
return (TbSeckillGoods)redisTemplate.boundHashOps("seckillGoods").get(id);
}
1
2
3
4
@RequestMapping("/findOneFromRedis")
public TbSeckillGoods findOneFromRedis(Long id){
return seckillGoodsService.findOneFromRedis(id);
}

增加超时时间设置

1
2
@Reference(timeout=10000)
private SeckillGoodsService seckillGoodsService;

前端代码

1
2
3
this.findOne=function(id){
return $http.get('seckillGoods/findOneFromRedis.do?id='+id);
}
1
2
3
4
5
6
7
8
//查询实体 
$scope.findOne=function(){
seckillGoodsService.findOne($location.search()['id']).success(
function(response){
$scope.entity= response;
}
);
}

秒杀倒计时效果

$interval服务简介

在AngularJS中$interval服务用来间歇性处理一些事情

格式为:

1
$interval(执行的函数,间隔的毫秒数,运行次数);

运行次数可以缺省,如果缺省则无限循环执行,取消执行用cancel方法

我先现在先做一个简单的例子:10秒倒计时 ,首先引入$interval , 控制层编写代码如下:

1
2
3
4
5
6
7
8
9
$scope.second = 10; 
time= $interval(function(){
if($scope.second>0){
$scope.second =$scope.second-1;
}else{
$interval.cancel(time);
alert("秒杀服务已结束");
}
},1000);

页面用表达式显示$scope.second的值

秒杀倒计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$scope.findOne=function(){	
seckillGoodsService.findOne($location.search()['id']).success(
function(response){
$scope.entity= response;
allsecond =Math.floor( ( new Date($scope.entity.endTime).getTime()- (new Date().getTime())) /1000); //总秒数
time= $interval(function(){
if(second>0){
second =second-1;
$scope.timeString=convertTimeString(allsecond);//转换时间字符串
}else{
$interval.cancel(time);
alert("秒杀服务已结束");
}
},1000);
}
);
}

//转换秒为 天小时分钟秒格式 XXX天 10:22:33
convertTimeString=function(allsecond){
var days= Math.floor( allsecond/(60*60*24));//天数
var hours= Math.floor( (allsecond-days*60*60*24)/(60*60) );//小数数
var minutes= Math.floor( (allsecond -days*60*60*24 - hours*60*60)/60 );//分钟数
var seconds= allsecond -days*60*60*24 - hours*60*60 -minutes*60; //秒数
var timeString="";
if(days>0){
timeString=days+"天 ";
}
return timeString+hours+":"+minutes+":"+seconds;
}

Math.floor()用于对小数取整,直接丢弃小数位数

修改页面seckill-item.html ,显示time的值

1
<span class="overtime"> 距离结束:{{timeString}}</span>

秒杀下单

需求分析

商品详细页点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。

后端代码

服务接口层

1
2
3
4
5
6
/**
* 提交订单
* @param seckillId
* @param userId
*/
public void submitOrder(Long seckillId,String userId);

服务实现层

Spring配置文件注入IdWorker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Autowired
private RedisTemplate redisTemplate;

@Autowired
private IdWorker idWorker;

@Override
public void submitOrder(Long seckillId, String userId) {
//从缓存中查询秒杀商品
TbSeckillGoods seckillGoods =(TbSeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(seckillId);
if(seckillGoods==null){
throw new RuntimeException("商品不存在");
}
if(seckillGoods.getStockCount()<=0){
throw new RuntimeException("商品已抢购一空");
}
//扣减(redis)库存
seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
redisTemplate.boundHashOps("seckillGoods").put(seckillId, seckillGoods);//放回缓存
if(seckillGoods.getStockCount()==0){//如果已经被秒光
seckillGoodsMapper.updateByPrimaryKey(seckillGoods);//同步到数据库
redisTemplate.boundHashOps("seckillGoods").delete(seckillId);
}
//保存(redis)订单
long orderId = idWorker.nextId();
TbSeckillOrder seckillOrder=new TbSeckillOrder();
seckillOrder.setId(orderId);
seckillOrder.setCreateTime(new Date());
seckillOrder.setMoney(seckillGoods.getCostPrice());//秒杀价格
seckillOrder.setSeckillId(seckillId);
seckillOrder.setSellerId(seckillGoods.getSellerId());
seckillOrder.setUserId(userId);//设置用户ID
seckillOrder.setStatus("0");//状态
redisTemplate.boundHashOps("seckillOrder").put(userId, seckillOrder);
}

控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RequestMapping("/submitOrder")
public Result submitOrder(Long seckillId){
String userId = SecurityContextHolder.getContext().getAuthentication().getName();
if("anonymousUser".equals(userId)){//如果未登录
return new Result(false, "用户未登录");
}
try {
seckillOrderService.submitOrder(seckillId, userId);
return new Result(true, "提交成功");
}catch (RuntimeException e) {
e.printStackTrace();
return new Result(false, e.getMessage());
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "提交失败");
}
}

前端代码

前端服务层

1
2
3
4
//提交订单
this.submitOrder=function(seckillId){
return $http.get('seckillOrder/submitOrder.do?seckillId='+seckillId);
}

前端控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
//提交订单
$scope.submitOrder=function(){
seckillGoodsService.submitOrder($scope.entity.id).success(
function(response){
if(response.success){
alert("下单成功,请在1分钟内完成支付");
location.href="pay.html";
}else{
alert(response.message);
}
}
);
}

页面

1
<a ng-click="submitOrder()" target="_blank" class="sui-btn  btn-danger addshopcar">秒杀抢购</a>

秒杀支付

需求分析

用户成功下单后,跳转到支付页面。支付页显示微信支付二维码。用户完成支付后,保存订单到数据库。

生成支付二维码

后端代码

1
2
3
4
5
/**
* 根据用户名查询秒杀订单
* @param userId
*/
public TbSeckillOrder searchOrderFromRedisByUserId(String userId);
1
2
3
4
@Override
public TbSeckillOrder searchOrderFromRedisByUserId(String userId) {
return (TbSeckillOrder) redisTemplate.boundHashOps("seckillOrder").get(userId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 支付控制层
* @author Administrator
*
*/
@RestController
@RequestMapping("/pay")
public class PayController {

@Reference
private WeixinPayService weixinPayService;

@Reference
private SeckillOrderService seckillOrderService;

/**
* 生成二维码
* @return
*/
@RequestMapping("/createNative")
public Map createNative(){
//获取当前用户
String userId=SecurityContextHolder.getContext().getAuthentication().getName();
//到redis查询秒杀订单
TbSeckillOrder seckillOrder = seckillOrderService.searchOrderFromRedisByUserId(userId);
//判断秒杀订单存在
if(seckillOrder!=null){
long fen= (long)(seckillOrder.getMoney().doubleValue()*100);//金额(分)
return weixinPayService.createNative(seckillOrder.getId()+"",+fen+"");
}else{
return new HashMap();
}
}
}

支付成功保存订单

后端代码

1
2
3
4
5
6
7
8
9
10
11
/**

* 支付成功保存订单

* @param userId

* @param orderId

*/

public void saveOrderFromRedisToDb(String userId,Long orderId,String transactionId);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void saveOrderFromRedisToDb(String userId, Long orderId, String transactionId) {
System.out.println("saveOrderFromRedisToDb:"+userId);
//根据用户ID查询日志
TbSeckillOrder seckillOrder = (TbSeckillOrder) redisTemplate.boundHashOps("seckillOrder").get(userId);
if(seckillOrder==null){
throw new RuntimeException("订单不存在");
}
//如果与传递过来的订单号不符
if(seckillOrder.getId().longValue()!=orderId.longValue()){
throw new RuntimeException("订单不相符");
}
seckillOrder.setTransactionId(transactionId);//交易流水号
seckillOrder.setPayTime(new Date());//支付时间
seckillOrder.setStatus("1");//状态
seckillOrderMapper.insert(seckillOrder);//保存到数据库
redisTemplate.boundHashOps("seckillOrder").delete(userId);//从redis中清除
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 查询支付状态
* @param out_trade_no
* @return
*/
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
//获取当前用户
String userId=SecurityContextHolder.getContext().getAuthentication().getName();
Result result=null;
int x=0;
while(true){
//调用查询接口
Map<String,String> map = weixinPayService.queryPayStatus(out_trade_no);
if(map==null){//出错
result=new Result(false, "支付出错");
break;
}
if(map.get("trade_state").equals("SUCCESS")){//如果成功
result=new Result(true, "支付成功");
seckillOrderService.saveOrderFromRedisToDb(userId, Long.valueOf(out_trade_no), map.get("transaction_id"));
break;
}
try {
Thread.sleep(3000);//间隔三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
x++;//设置超时时间为5分钟
if(x>100){
result=new Result(false, "二维码超时");
break;
}
}
return result;
}

前端代码

1
queryPayStatus(response.out_trade_no);//查询支付状态

订单超时处理

当用户下单后5分钟尚未付款应该释放订单,增加库存

删除缓存中的订单

1
2
3
4
5
6
/**
* 从缓存中删除订单
* @param userId
* @param orderId
*/
public void deleteOrderFromRedis(String userId,Long orderId);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void deleteOrderFromRedis(String userId, Long orderId) {
//根据用户ID查询日志
TbSeckillOrder seckillOrder = (TbSeckillOrder) redisTemplate.boundHashOps("seckillOrder").get(userId);
if(seckillOrder!=null &&
seckillOrder.getId().longValue()== orderId.longValue() ){
redisTemplate.boundHashOps("seckillOrder").delete(userId);//删除缓存中的订单
//恢复库存
//1.从缓存中提取秒杀商品
TbSeckillGoods seckillGoods=(TbSeckillGoods)redisTemplate.boundHashOps("seckillGoods").get(seckillOrder.getSeckillId());
if(seckillGoods!=null){
seckillGoods.setStockCount(seckillGoods.getStockCount()+1);
redisTemplate.boundHashOps("seckillGoods").put(seckillOrder.getSeckillId(), seckillGoods);//存入缓存
}
}
}

关闭微信订单

1
2
3
4
5
6
/**
* 关闭支付
* @param out_trade_no
* @return
*/
public Map closePay(String out_trade_no);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public Map closePay(String out_trade_no) {
Map param=new HashMap();
param.put("appid", appid);//公众账号ID
param.put("mch_id", partner);//商户号
param.put("out_trade_no", out_trade_no);//订单号
param.put("nonce_str", WXPayUtil.generateNonceStr());//随机字符串
String url="https://api.mch.weixin.qq.com/pay/closeorder";
try {
String xmlParam = WXPayUtil.generateSignedXml(param, partnerkey);
HttpClient client=new HttpClient(url);
client.setHttps(true);
client.setXmlParam(xmlParam);
client.post();
String result = client.getContent();
Map<String, String> map = WXPayUtil.xmlToMap(result);
System.out.println(map);
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

超时调用服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 查询支付状态
* @param out_trade_no
* @return
*/
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
//获取当前用户
String userId=SecurityContextHolder.getContext().getAuthentication().getName();
Result result=null;
int x=0;
while(true){
........
try {
Thread.sleep(3000);//间隔三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//不让循环无休止地运行定义变量,如果超过了这个值则退出循环,设置时间为1分钟
x++;
if(x>20){
result=new Result(false, "二维码超时");
//1.调用微信的关闭订单接口(学员实现)
Map<String,String> payresult = weixinPayService.closePay(out_trade_no);
if( !"SUCCESS".equals(payresult.get("result_code")) ){//如果返回结果是正常关闭
if("ORDERPAID".equals(payresult.get("err_code"))){
result=new Result(true, "支付成功");
seckillOrderService.saveOrderFromRedisToDb(userId, Long.valueOf(out_trade_no), map.get("transaction_id"));
}
}
if(result.isSuccess()==false){
System.out.println("超时,取消订单");
//2.调用删除
seckillOrderService.deleteOrderFromRedis(userId, Long.valueOf(out_trade_no));
}
break;
}
}
return result;
}

前端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//查询支付状态 
queryPayStatus=function(out_trade_no){
payService.queryPayStatus(out_trade_no).success(
function(response){
if(response.success){
location.href="paysuccess.html#?money="+$scope.money;
}else{
if(response.message=='二维码超时'){
location.href="payTimeOut.html";
}else{
location.href="payfail.html";
}
}
}
);
}

二维码

什么是二维码

二维码又称QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。

二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。

二维码优势

  • 信息容量大, 可以容纳多达1850个大写字母或2710个数字或500多个汉字
  • 应用范围广, 支持文字,声音,图片,指纹等等…
  • 容错能力强, 即使图片出现部分破损也能使用
  • 成本低, 容易制作

二维码容错级别

  • L级(低) 7%的码字可以被恢复。
  • M级(中) 的码字的15%可以被恢复。
  • Q级(四分)的码字的25%可以被恢复。
  • H级(高) 的码字的30%可以被恢复。

二维码生成插件qrious

qrious是一款基于HTML5 Canvas的纯JS二维码生成插件。通过qrious.js可以快速生成各种二维码,你可以控制二维码的尺寸颜色,还可以将生成的二维码进行Base64编码。

qrious.js二维码插件的可用配置参数如下:

参数 类型 默认值 描述
background String “white” 二维码的背景颜色。
foreground String “black” 二维码的前景颜色。
level String “L” 二维码的误差校正级别(L, M, Q, H)。
mime String “image/png” 二维码输出为图片时的MIME类型。
size Number 100 二维码的尺寸,单位像素。
value String “” 需要编码为二维码的值

下面的代码即可生成一张二维码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<title>二维码入门小demo</title>
</head>
<body>
<img id="qrious">
<script src="qrious.min.js"></script>
<script>
var qr = new QRious({
element:document.getElementById('qrious'),
size:250,
level:'H',
value:'http://www.zhenganwen.top'
});
</script>
</body>
</html>

微信扫码支付

微信扫码支付是微信支付提供的支付方式之一,我们只需调用微信支付系统暴露的接口便可实现获取用户要扫码支付的链接和查询用户是否支付成功两大功能。

微信扫码支付申请

微信扫码支付是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站支付、实体店单品或订单支付、媒体广告支付等场景。

申请步骤:

第一步:注册公众号(类型须为:服务号)

请根据营业执照类型选择以下主体注册:个体工商户| 企业/公司| 政府| 媒体| 其他类型

第二步:认证公众号

公众号认证后才可申请微信支付,认证费:300元/次。

第三步:提交资料申请微信支付

登录公众平台,点击左侧菜单【微信支付】,开始填写资料等待审核,审核时间为1-5个工作日内。

第四步:开户成功,登录商户平台进行验证

资料审核通过后,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证。

第五步:在线签署协议

本协议为线上电子协议,签署后方可进行交易及资金结算,签署完立即生效。

开发文档

微信支付接口调用的整体思路:

按API要求组装参数,以XML方式发送(POST)给微信支付接口(URL),微信支付接口也是以XML方式给予响应。程序根据返回的结果(其中包括支付URL)生成二维码或判断订单状态。

在线微信支付开发文档:

https://pay.weixin.qq.com/wiki/doc/api/index.html

本文将会用到”统一下单”和”查询订单”两组API

名词解析

  1. appid:微信公众账号或开放平台APP的唯一标识

  2. mch_id:商户号 (下文配置文件中的partner)

  3. partnerkey:商户密钥

  4. sign:数字签名, 根据微信官方提供的密钥和一套算法生成的一个加密信息, 就是为了保证交易的安全性

微信支付SDK

微信支付提供了SDK, 大家下载后打开源码,install到本地仓库。

使用微信支付SDK,在maven工程中引入依赖 “:

1
2
3
4
5
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>

我们主要会用到微信支付SDK的以下功能:

(1)获取随机字符串

1
WXPayUtil.generateNonceStr()

(2)Map转换为XML字符串(自动添加签名)

1
WXPayUtil.generateSignedXml(param, partnerkey)

(3)XML字符串转换为Map

1
WXPayUtil.xmlToMap(result)

HttpClient工具类

HttpClient是Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议。HttpClient已经应用在很多的项目中,比如Apache Jakarta上很著名的另外两个开源项目Cactus和HTMLUnit都使用了HttpClient。

HttpClient通俗的讲就是模拟了浏览器的行为,如果我们需要在后端向某一地址提交数据获取结果,就可以使用HttpClient.

关于HttpClient(原生)具体的使用不属于我们本章的学习内容,我们这里这里为了简化HttpClient的使用,提供了工具类HttpClient(对原生HttpClient进行了封装)

工具类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package cn.itcast.utils;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

/**
* http请求客户端
*
* @author Administrator
*
*/
public class HttpClient {
private String url;
private Map<String, String> param;
private int statusCode;
private String content;
private String xmlParam;
private boolean isHttps;

public boolean isHttps() {
return isHttps;
}

public void setHttps(boolean isHttps) {
this.isHttps = isHttps;
}

public String getXmlParam() {
return xmlParam;
}

public void setXmlParam(String xmlParam) {
this.xmlParam = xmlParam;
}

public HttpClient(String url, Map<String, String> param) {
this.url = url;
this.param = param;
}

public HttpClient(String url) {
this.url = url;
}

public void setParameter(Map<String, String> map) {
param = map;
}

public void addParameter(String key, String value) {
if (param == null)
param = new HashMap<String, String>();
param.put(key, value);
}

public void post() throws ClientProtocolException, IOException {
HttpPost http = new HttpPost(url);
setEntity(http);
execute(http);
}

public void put() throws ClientProtocolException, IOException {
HttpPut http = new HttpPut(url);
setEntity(http);
execute(http);
}

public void get() throws ClientProtocolException, IOException {
if (param != null) {
StringBuilder url = new StringBuilder(this.url);
boolean isFirst = true;
for (String key : param.keySet()) {
if (isFirst)
url.append("?");
else
url.append("&");
url.append(key).append("=").append(param.get(key));
}
this.url = url.toString();
}
HttpGet http = new HttpGet(url);
execute(http);
}

/**
* set http post,put param
*/
private void setEntity(HttpEntityEnclosingRequestBase http) {
if (param != null) {
List<NameValuePair> nvps = new LinkedList<NameValuePair>();
for (String key : param.keySet())
nvps.add(new BasicNameValuePair(key, param.get(key))); // 参数
http.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8)); // 设置参数
}
if (xmlParam != null) {
http.setEntity(new StringEntity(xmlParam, Consts.UTF_8));
}
}

private void execute(HttpUriRequest http) throws ClientProtocolException,
IOException {
CloseableHttpClient httpClient = null;
try {
if (isHttps) {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(null, new TrustStrategy() {
// 信任所有
public boolean isTrusted(X509Certificate[] chain,
String authType)
throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext);
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf)
.build();
} else {
httpClient = HttpClients.createDefault();
}
CloseableHttpResponse response = httpClient.execute(http);
try {
if (response != null) {
if (response.getStatusLine() != null)
statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
// 响应内容
content = EntityUtils.toString(entity, Consts.UTF_8);
}
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
httpClient.close();
}
}

public int getStatusCode() {
return statusCode;
}

public String getContent() throws ParseException, IOException {
return content;
}
}

HttpClient工具类使用的步骤

1
2
3
4
5
HttpClient client=new HttpClient(请求的url地址);
client.setHttps(true);//是否是https协议
client.setXmlParam(xmlParam);//发送的xml数据
client.post();//执行post请求
String result = client.getContent(); //获取结果

微信支付二维码生成

实现思路

我们通过HttpClient工具类实现对远程支付接口的调用。

接口链接:https://api.mch.weixin.qq.com/pay/unifiedorder

具体参数参见“统一下单”API, 构建参数发送给统一下单的url ,返回的信息中有支付url,根据url生成二维码,显示的订单号和金额也在返回的信息中。

后端代码实现

服务层接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.pinyougou.pay.service;
import java.util.Map;
/**
* 微信支付接口
* @author Administrator
*
*/
public interface WeixinPayService {

/**
* 生成微信支付二维码
* @param out_trade_no 订单号
* @param total_fee 金额(分)
* @return
*/
public Map createNative(String out_trade_no,String total_fee);
}

服务实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Service
public class WeixinPayServiceImpl implements WeixinPayService {

@Value("${appid}")
private String appid;
//以下两个参数在你申请微信扫码后方可获取
@Value("${partner}")
private String partner;
@Value("${partnerkey}")
private String partnerkey;

/**
* 生成二维码
* @return
*/
public Map createNative(String out_trade_no,String total_fee){
//1.创建参数
Map<String,String> param=new HashMap();//创建参数
param.put("appid", appid);//公众号
param.put("mch_id", partner);//商户号
param.put("nonce_str", WXPayUtil.generateNonceStr());//随机字符串
param.put("body", "品优购");//商品描述
param.put("out_trade_no", out_trade_no);//商户订单号
param.put("total_fee",total_fee);//总金额(分)
param.put("spbill_create_ip", "127.0.0.1");//IP
param.put("notify_url", "http://www.zhenganwen.top");//回调地址(随便写),由我们自己通过后续查询支付状态来进行页面的跳转
param.put("trade_type", "NATIVE");//交易类型
try {
//2.生成要发送的xml
String xmlParam = WXPayUtil.generateSignedXml(param, partnerkey);
System.out.println(xmlParam);
HttpClient client=new HttpClient("https://api.mch.weixin.qq.com/pay/unifiedorder");
client.setHttps(true);
client.setXmlParam(xmlParam);
client.post();
//3.获得结果
String result = client.getContent();
System.out.println(result);
Map<String, String> resultMap = WXPayUtil.xmlToMap(result);
Map<String, String> map=new HashMap<>();
map.put("code_url", resultMap.get("code_url"));//支付地址
map.put("total_fee", total_fee);//总金额
map.put("out_trade_no",out_trade_no);//订单号
return map;
} catch (Exception e) {
e.printStackTrace();
return new HashMap<>();
}
}
}

控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 支付控制层
* @author Administrator
*
*/
@RestController
@RequestMapping("/pay")
public class PayController {
@Reference
private WeixinPayService weixinPayService;

/**
* 生成二维码
* @return
*/
@RequestMapping("/createNative")
public Map createNative(){
IdWorker idworker=new IdWorker();
return weixinPayService.createNative(idworker.nextId()+"","1");
}
}

前端代码实现

服务层

1
2
3
4
5
6
app.service('payService',function($http){
//本地支付
this.createNative=function(){
return $http.get('pay/createNative.do');
}
});

控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.controller('payController' ,function($scope ,payService){	
//本地生成二维码
$scope.createNative=function(){
payService.createNative().success(
function(response){
$scope.money= (response.total_fee/100).toFixed(2) ; //金额
$scope.out_trade_no= response.out_trade_no;//订单号
//二维码
var qr = new QRious({
element:document.getElementById('qrious'),
size:250,
level:'H',
value:response.code_url
});
}
);
}
});

页面

设置二维码图片的ID

1
2
3
4
5
6
7
8
<p class="red"></p>                      
<div class="fl code">
<img id="qrious">
<div class="saosao">
<p>请使用微信扫一扫</p>
<p>扫描二维码支付</p>
</div>
</div>

显示订单号

1
订单号:{{out_trade_no}}

显示金额

1
<em  class="orange money">¥{{money}}</em>

检测支付状态

需求分析

当用户支付成功后跳转到成功页面,当返回异常时跳转到错误页面.

实现思路

我们通过HttpClient工具类实现对远程支付接口的调用。

接口链接:https://api.mch.weixin.qq.com/pay/orderquery

具体参数参见“查询订单”API, 我们在controller方法中轮询调用查询订单(间隔3秒),当返回状态为success时,我们会在controller方法返回结果。前端代码收到结果后跳转到成功页面。

后端代码

服务接口层

1
2
3
4
5
/**
* 查询支付状态
* @param out_trade_no
*/
public Map queryPayStatus(String out_trade_no);

服务实现层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public Map queryPayStatus(String out_trade_no) {
Map param=new HashMap();
param.put("appid", appid);//公众账号ID
param.put("mch_id", partner);//商户号
param.put("out_trade_no", out_trade_no);//订单号
param.put("nonce_str", WXPayUtil.generateNonceStr());//随机字符串
String url="https://api.mch.weixin.qq.com/pay/orderquery";
try {
String xmlParam = WXPayUtil.generateSignedXml(param, partnerkey);
HttpClient client=new HttpClient(url);
client.setHttps(true);
client.setXmlParam(xmlParam);
client.post();
String result = client.getContent();
Map<String, String> map = WXPayUtil.xmlToMap(result);
System.out.println(map);
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 查询支付状态
* @param out_trade_no
* @return
*/
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
Result result=null;
while(true){
//调用查询接口
Map<String,String> map = weixinPayService.queryPayStatus(out_trade_no);
if(map==null){//出错
result=new Result(false, "支付出错");
break;
}
if(map.get("trade_state").equals("SUCCESS")){//如果成功
result=new Result(true, "支付成功");
break;
}
try {
Thread.sleep(3000);//间隔三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return result;
}

前端代码

服务层

1
2
3
4
//查询支付状态
this.queryPayStatus=function(out_trade_no){
return $http.get('pay/queryPayStatus.do?out_trade_no='+out_trade_no);
}

控制层

1
2
3
4
5
6
7
8
9
10
11
12
//查询支付状态 
queryPayStatus=function(out_trade_no){
payService.queryPayStatus(out_trade_no).success(
function(response){
if(response.success){
location.href="paysuccess.html";
}else{
location.href="payfail.html";
}
}
);
}

在createNative方法的回调方法中调用此查询方法:

1
2
3
4
5
6
7
8
9
//本地生成二维码
$scope.createNative=function(){
payService.createNative().success(
function(response){
..........
queryPayStatus(response.out_trade_no);//查询支付状态
}
);
}

查询时间限制

问题分析

如果用户到了二维码页面一直未支付,或是关掉了支付页面,我们的代码会一直循环调用微信接口,这样会对程序造成很大的压力。所以我们要加一个时间限制或是循环次数限制,当超过时间或次数时,跳出循环。

修改后端控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("/queryPayStatus")
public Result queryPayStatus(String out_trade_no){
Result result=null;
int x=0;
while(true){
//调用查询接口
.......
try {
Thread.sleep(3000);//间隔三秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//为了不让循环无休止地运行,我们定义一个循环变量,如果这个变量超过了这个值则退出循环,设置时间为5分钟
x++;
if(x>=100){
result=new Result(false, "二维码超时");
break;
}
}
return result;
}

修改前端控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//查询支付状态 
queryPayStatus=function(out_trade_no){
payService.queryPayStatus(out_trade_no).success(
function(response){
if(response.success){
location.href="paysuccess.html";
}else{
if(response.message=='二维码超时'){
$scope.createNative();//重新生成二维码
}else{
location.href="payfail.html";
}
}
}
);
}

分布式ID生成器

我们采用的是开源的twitter( 非官方中文惯称:推特.是国外的一个网站,是一个社交网络及微博客服务) 的snowflake(雪花)算法。

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package util;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;

/**
* <p>名称:IdWorker.java</p>
* <p>描述:分布式自增长ID</p>
* <pre>
* Twitter的 Snowflake JAVA实现方案
* </pre>
* 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用:
* 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
* 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,
* 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),
* 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
* 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),
* 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。
* <p>
* 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))
*
* @author Polim
*/
public class IdWorker {
// 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
private final static long twepoch = 1288834974657L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 机器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 数据中心ID最大值
private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 毫秒内自增位
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
/* 上次生产id时间戳 */
private static long lastTimestamp = -1L;
// 0,并发控制
private long sequence = 0L;

private final long workerId;
// 数据标识id部分
private final long datacenterId;

public IdWorker(){
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* @param workerId
* 工作机器ID
* @param datacenterId
* 序列号
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
*
* @return
*/
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}

if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift) | sequence;

return nextId;
}

private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}

private long timeGen() {
return System.currentTimeMillis();
}

/**
* <p>
* 获取 maxWorkerId
* </p>
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuffer mpid = new StringBuffer();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (!name.isEmpty()) {
/*
* GET jvmPid
*/
mpid.append(name.split("@")[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}

/**
* <p>
* 数据标识id部分
* </p>
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1])
| (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
} catch (Exception e) {
System.out.println(" getDatacenterId: " + e.getMessage());
}
return id;
}


}

使用方法

1.创建IdWorder对象,可以在构造参数中指定机器ID和序列号,若未提供则随机生成

2.调用nextId()方法,会根据当前时间戳和构造参数生成不一样的ID

跨域解决方案CORS

​ CORS是一个W3C标准,全称是”跨域资源共享“(Cross-origin resource sharing)。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

​ 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信

请求过程

Preflight Request

Preflight Response

然后服务器端给我们返回一个Preflight Response :

这个预请求和预响应的过程就像进别人办公室前的敲门和回应的“请进”。浏览器要事先知道服务器是否允许跨域请求。然后接下来才是正在业务所需的请求和响应

代码实现

服务器端设置响应头信息

设置可被跨域请求的域

1
response.setHeader("Access-Control-Allow-Origin", "http://localhost:9105");

response.setHeader("Access-Control-Allow-Origin", "*");可接受任何域的跨域请求,这里的是一个协议、域名和端口号的组合。

Access-Control-Allow-OriginHTML5中定义的一种解决资源跨域的策略。它是通过服务器端返回带有Access-Control-Allow-Origin标识的Response header,用来解决资源的跨域权限问题。

注意:服务器接口方法设置此响应头之后,前端js就能跨域请求该接口了,但是只能以GET的方式

设置cookie可被读写

CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段另一方面,开发者必须在AJAX请求中打开withCredentials属性。否则,即使服务器同意浏览器发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

服务器处理如下:

1
response.setHeader("Access-Control-Allow-Credentials", "true");

跨域js请求如下:

1
2
3
4
5
6
7
8
9
//添加商品到购物车
$scope.addToCart=function(){
$http.get('http://localhost:9107/cart/addGoodsToCartList.do?itemId='
+ $scope.sku.id +'&num='+$scope.num,{'withCredentials':true}).success(
function(response){
.......
}
);
}

注意,当使用response设置Access-Control-Allow-Credentialstrue之后,前面Access-Control-Allow-Origin的值就不能是*,而只能是一个具体的域了,因为cookie是和域绑定的

SpringMVC对CORS的支持

SpringMVC的版本在4.2或以上版本,可以使用注解实现跨域, 我们只需要在需要跨域的方法上添加注解@CrossOrigin即可 :

1
2
@CrossOrigin(origins="http://localhost:9105",allowCredentials="true")
@RequestMapping

allowCredentials="true" 可以缺省

购物车需求分析与解决方案

需求分析

​ 用户在商品详细页点击加入购物车,提交商品SKU编号和购买数量,添加到购物车。购物车展示页面如下:

实现思路

​ 购物车数据的存储结构如下(总购物车包含若干商家购物车):

​ 当用户在未登录的情况下,将此购物车存入cookies , 在用户登陆的情况下,将购物车数据存入redis 。如果用户登陆时,cookies中存在购物车,需要将cookies的购物车合并到redis中存储.

购物车实体类VO

1
2
3
4
5
6
public class Cart implements Serializable{
private String sellerId;//商家ID
private String sellerName;//商家名称
private List<TbOrderItem> orderItemList;//购物车明细
//getter and setter ......
}

​ 这个类是对每个商家的购物车进行的封装

Cookie存储购物车

需求分析

​ 使用cookie存储购物车数据。服务层负责逻辑,控制层负责读写cookie

服务接口层

1
2
3
public interface CartService {
public List<Cart> addGoodsToCartList(List<Cart> cartList,Long itemId,Integer num );
}

服务实现层

实现思路:

  • 1.根据商品SKU ID查询SKU商品信息
  • 2.获取商家ID
  • 3.根据商家ID判断购物车列表中是否存在该商家的购物车
  • 4.如果购物车列表中不存在该商家的购物车
    • 4.1 新建购物车对象
    • 4.2 将新建的购物车对象添加到购物车列表
  • 5.如果购物车列表中存在该商家的购物车,查询购物车明细列表中是否存在该商品
    • 5.1. 如果没有,新增购物车明细
    • 5.2. 如果有,在原购物车明细上添加数量,更改金额

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//增加明细到购物车中
@Override
public List<Cart> addItem2Cart(Long itemId, Integer num, List<Cart> cartList) {

TbItem item = itemMapper.selectByPrimaryKey(itemId);
if (item == null) {
throw new RuntimeException("该商品不存在:" + itemId);
}
if ("1".equals(item.getStatus()) == false) {
throw new RuntimeException("商品状态不正常!");
}

//若购物车有该商品所属的商家
Cart cart = searchCartFromCartListBySellerId(item.getSellerId(), cartList);
if (cart != null) {
//1、若商家购物车已有该商品
TbOrderItem orderItem = searchOrderItemFromCart(item, cart);
if (orderItem != null) {
//若已有该商品,则累加数量并重新计算小计
orderItem.setNum(orderItem.getNum() + num);
orderItem.setTotalFee(new BigDecimal( orderItem.getPrice().doubleValue() * orderItem.getNum()));
//若数量为负,则为拣出购物车,若全部拣出,则从该购物车中删除该条目
if (orderItem.getNum() <= 0) {
cart.getOrderItemList().remove(orderItem);
}
//若该商家购物车中没有条目了,则从总购物车中删除商家购物车
if (cart.getOrderItemList().size() == 0) {
cartList.remove(cart);
}
//操作完成
return cartList;
}

//2、若商家购物车没有该商品,则新建条目并添加到该购物车中
cart.getOrderItemList().add(createOrderItem(item, num));
//操作完毕
return cartList;
}

//3、若购物车没有该商品商家,则新建商家购物车并添加条目,最后将商家购物车添加到总购物车
cart = new Cart();
cart.setSellerId(item.getSellerId());
cart.setSellerName(sellerMapper.selectByPrimaryKey(cart.getSellerId()).getName());
List<TbOrderItem> orderItemList = new ArrayList<TbOrderItem>();
orderItemList.add(createOrderItem(item, num));
cart.setOrderItemList(orderItemList);
cartList.add(cart);
//操作完毕
return cartList;
}

//根据SKU和数量创建明细
private TbOrderItem createOrderItem(TbItem item,Integer num) {
if(num <= 0){
throw new RuntimeException("数量非法!");
}
TbOrderItem orderItem = new TbOrderItem();
orderItem.setItemId(item.getId());
orderItem.setGoodsId(item.getGoodsId());
orderItem.setSellerId(item.getSellerId());
orderItem.setPicPath(item.getImage());
orderItem.setTitle(item.getTitle());
orderItem.setPrice(item.getPrice());
orderItem.setNum(num);
orderItem.setTotalFee(new BigDecimal( item.getPrice().doubleValue() * orderItem.getNum().doubleValue()+""));
return orderItem;
}

//从商家购物车中根据SKU ID查询明细
private TbOrderItem searchOrderItemFromCart(TbItem item, Cart cart) {
if (cart != null) {
for (TbOrderItem orderItem : cart.getOrderItemList()) {
if (orderItem.getItemId().equals(item.getId())) {
return orderItem;
}
}
}
return null;
}

//从总购物车中根据商家ID查商家购物车
private Cart searchCartFromCartListBySellerId(String sellerId, List<Cart> cartList) {
for (Cart cart : cartList) {
if (cart.getSellerId().equals(sellerId)) {
return cart;
}
}
return null;
}

控制层

  • 提供增加/拣出SKU到购物车和获取购物车数据的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Reference
private CartService cartService;

public static final int ONE_DAY=60*60*24;

@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;

public static final String UTF_8 = "UTF-8";
public static final String CART_COOKIE_NAME = "cart";

@RequestMapping("/addGoodsToCartList")
public Result addGoodsToCartList(Long itemId,@RequestParam(defaultValue = "1") Integer num) {
try {

//调用服务添加商品到购物车
List<Cart> cartList = cartService.addItem2Cart(itemId, num, getCartListFromCookie());
saveCartList2Cookie(cartList);
return Result.success();
} catch (Exception e) {
e.printStackTrace();
return Result.fail();
}
}

public static final String JSON_ARRAY_PREFIX = "[";

@RequestMapping("/findCartList")
public List<Cart> findCartList() {
//从cookie中取cartList
String cartListJson = CookieUtil.getCookieValue(request, CART_COOKIE_NAME, UTF_8);
if (StringUtils.isEmpty(cartListJson) || cartListJson.startsWith(JSON_ARRAY_PREFIX)==false) {
cartListJson = "[]";
}
return JSON.parseArray(cartListJson, Cart.class);
}

public void saveCartList2Cookie(List<Cart> cartList) {
//将购物车写入cookie
CookieUtil.setCookie(request, response, CART_COOKIE_NAME,JSON.toJSONString(cartList),ONE_DAY, UTF_8);
}

购物车前端代码

需求分析

​ 实现购物车页面的展示与相关操作

​ 可以实现购物车列表、数量的增减与移除以及更新小计、合计

购物车列表

前端服务层

  • cartService
1
2
3
4
5
6
app.service('cartService',function($http){
//购物车列表
this.findCartList=function(){
return $http.get('cart/findCartList.do');
}
});
  • cartController.js
1
2
3
4
5
6
7
8
9
10
app.controller('cartController',function($scope,cartService){
//查询购物车列表
$scope.findCartList=function(){
cartService.findCartList().success(
function(response){
$scope.cartList=response;
}
);
}
});
  • 页面加载时请求显示购物车列表
1
<body ng-app="myApp" ng-controller="cartController" ng-init="findCartList()">
  • 遍历cartList
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<div class="cart-item-list" ng-repeat="cart in cartList">
<div class="cart-shop">
<input type="checkbox" name="" id="" value="" />
<span class="shopname self">{{cart.sellerName}}【商家ID:{{cart.sellerId}}】</span>
</div>
<div class="cart-body">
<div class="cart-list" ng-repeat="orderItem in cart.orderItemList">
<ul class="goods-list yui3-g">
<li class="yui3-u-1-24">
<input type="checkbox" name="" id="" value="" />
</li>
<li class="yui3-u-11-24">
<div class="good-item">
<div class="item-img"><img src="{{orderItem.picPath}}" /></div>
<div class="item-msg">
{{orderItem.title}}
</div>
</div>
</li>
<li class="yui3-u-1-8"><span class="price">{{orderItem.price.toFixed(2)}}</span></li>
<li class="yui3-u-1-8">
<a href="javascript:void(0)" class="increment mins">-</a>
<input autocomplete="off" type="text" ng-model="orderItem.num" minnum="1" class="itxt" />
<a href="javascript:void(0)" class="increment plus">+</a>
</li>
<li class="yui3-u-1-8"><span class="sum">{{orderItem.totalFee.toFixed(2)}}</span></li>
<li class="yui3-u-1-8">
<a href="#none">删除</a><br />
<a href="#none">移到我的关注</a>
</li>
</ul>
</div>
</div>
</div>

购物车数量增减与移除

  • cartService.js
1
2
3
4
//添加商品到购物车
this.addGoodsToCartList=function(itemId,num){
return $http.get('cart/addGoodsToCartList.do?itemId='+itemId+'&num='+num);
}
  • cartController.js
1
2
3
4
5
6
7
8
9
10
11
12
//添加商品到购物车
$scope.addGoodsToCartList=function(itemId,num){
cartService.addGoodsToCartList(itemId,num).success(
function(response){
if(response.success){
$scope.findCartList();//刷新列表
}else{
alert(response.message);//弹出错误提示
}
}
);
}
  • 页面调用
1
2
3
4
5
6
7
8
9
10
11
<li class="yui3-u-1-8">
<a href="javascript:void(0)" ng-click="addGoodsToCartList(orderItem.itemId,-1)" class="increment mins">
-
</a>

<input autocomplete="off" type="text" ng-model="orderItem.num" minnum="1" class="itxt" />

<a href="javascript:void(0)" ng-click="addGoodsToCartList(orderItem.itemId,1)" class="increment plus">
+
</a>
</li>

实现删除功能

1
2
3
4
<li class="yui3-u-1-8">
<a href="#none" ng-click="addGoodsToCartList(orderItem.itemId,-orderItem.num)">删除</a><br />
<a href="#none">移到我的关注</a>
</li>

更新合计

  • 修改cartService.js
1
2
3
4
5
6
7
8
9
10
11
12
13
//求合计
this.sum=function(cartList){
var totalValue={totalNum:0, totalMoney:0.00 };//合计实体
for(var i=0;i<cartList.length;i++){
var cart=cartList[i];
for(var j=0;j<cart.orderItemList.length;j++){
var orderItem=cart.orderItemList[j];//购物车明细
totalValue.totalNum+=orderItem.num;
totalValue.totalMoney+= orderItem.totalFee;
}
}
return totalValue;
}
  • cartController
1
2
3
4
5
6
7
8
9
//查询购物车列表
$scope.findCartList=function(){
cartService.findCartList().success(
function(response){
$scope.cartList=response;
$scope.totalValue=cartService.sum($scope.cartList);//求合计数
}
);
}
  • 页面
1
2
3
4
5
6
<div class="chosed">已选择<span>{{totalValue.totalNum}}</span>件商品</div>
<div class="sumprice">
<span>
<em>总价(不含运费) :</em><i class="summoney">¥{{totalValue.totalMoney}}</i>
</span>
</div>

Redis存储购物车

获取当前登录人账号

配置文件

  • 修改spring-security.xml (删一行,加一行):
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--
删除此行,访问/cart/*.do时相当于绕道走,没有进过security,获取登录名会抛出空指针异常
<http pattern="/cart/*.do" security="none"></http>
-->

<http use-expressions="false" entry-point-ref="casProcessingFilterEntryPoint">
<!-- 增加此行,访问/cart/*.do时相当于光明正大走,经过了security,security发一张绿卡,获取登录名为anonymousUser -->
<intercept-url pattern="/cart/*.do" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/**" access="ROLE_USER"/>
<custom-filter position="CAS_FILTER" ref="casAuthenticationFilter" />
<custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</http>

access="IS_AUTHENTICATED_ANONYMOUSLY" 用于设置资源可以在不登陆时可以访问。

此配置与 security=”none”的区别在于当用户未登陆时获取登陆人账号的值(SecurityContext.getContext().getAuthtication().getName())为anonymousUser ,而security=”none”的话,无论是否登陆都不能获取登录人账号的值。

代码实现

pinyougou-cart-webfindCartList和addGoodsToCartList方法中,获取用户名:

1
2
//得到登陆人账号,判断当前是否有人登陆
String username = SecurityContextHolder.getContext().getAuthentication().getName();

测试:当用户未登陆时,username的值为anonymousUser,若按之前的配置则为null

远程购物车存取

服务接口层

pinyougou-cart-interfaceCartService.java定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 从redis中查询购物车
* @param username
* @return
*/
public List<Cart> findCartListFromRedis(String username);

/**
* 将购物车保存到redis
* @param username
* @param cartList
*/
public void saveCartListToRedis(String username,List<Cart> cartList);

服务实现层

pinyougou-cart-serviceCartServiceImpl.java实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Autowired
private RedisTemplate redisTemplate;
@Override
public List<Cart> findCartListFromRedis(String username) {
System.out.println("从redis中提取购物车数据....."+username);
List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(username);
if(cartList==null){
cartList=new ArrayList();
}
return cartList;
}
@Override
public void saveCartListToRedis(String username, List<Cart> cartList) {
System.out.println("向redis存入购物车数据....."+username);
redisTemplate.boundHashOps("cartList").put(username, cartList);
}

控制层

修改CartController.javafindCartList方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 购物车列表
* @param request
* @return
*/
@RequestMapping("/findCartList")
public List<Cart> findCartList(){
String username = SecurityContextHolder.getContext().getAuthentication().getName();
if(username.equals("anonymousUser")){//如果未登录
//读取本地购物车//
..........
return cartList_cookie;
}else{//如果已登录
List<Cart> cartList_redis =cartService.findCartListFromRedis(username);//从redis中提取
return cartList_redis;
}
}

修改addGoodsToCartList方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 添加商品到购物车
* @param request
* @param response
* @param itemId
* @param num
* @return
*/
@RequestMapping("/addGoodsToCartList")
public Result addGoodsToCartList(Long itemId,Integer num){
String username = SecurityContextHolder.getContext().getAuthentication().getName();
System.out.println("当前登录用户:"+username);
try {
List<Cart> cartList =findCartList();//获取购物车列表
cartList = cartService.addGoodsToCartList(cartList, itemId, num);
if(username.equals("anonymousUser")){ //如果是未登录,保存到cookie
util.CookieUtil.setCookie(request, response, "cartList", JSON.toJSONString(cartList),3600*24 ,"UTF-8");
System.out.println("向cookie存入数据");
}else{//如果是已登录,保存到redis
cartService.saveCartListToRedis(username, cartList);
}
return new Result(true, "添加成功");
} catch (RuntimeException e) {
e.printStackTrace();
return new Result(false, e.getMessage());
}catch (Exception e) {
e.printStackTrace();
return new Result(false, "添加失败");
}
}

为避免调用远程服务超时,我们可以将过期时间改为6秒(默认为1秒)

1
2
@Reference(timeout=6000)
private CartService cartService;

跳板页

(1)创建跳板页:pinyougou-cart-web 工程新建login.html ,页面添加脚本

1
2
3
<script type="text/javascript">
location.href="cart.html";
</script>

(2)购物车页面链接到跳板页

1
<a href="login.html">登录</a>

这里用了个小技巧:当用户点击登录时,由于login.html在security的拦截规则之中,因此跳到cas系统登录页,当登录之后回调调往login.html,而login.html又跳往cart.html

购物车合并

服务接口层

pinyougou-cart-interface工程的CartService.java定义方法

1
2
3
4
5
6
7
/**
* 合并购物车
* @param cartList1
* @param cartList2
* @return
*/
public List<Cart> mergeCartList(List<Cart> cartList1,List<Cart> cartList2);

服务实现层

pinyougou-cart-service工程CartServiceImpl.java实现方法

1
2
3
4
5
6
7
8
9
public List<Cart> mergeCartList(List<Cart> cartList1, List<Cart> cartList2) {
System.out.println("合并购物车");
for(Cart cart: cartList2){
for(TbOrderItem orderItem:cart.getOrderItemList()){
cartList1= addGoodsToCartList(cartList1,orderItem.getItemId(),orderItem.getNum());
}
}
return cartList1;
}

在何时合并?当用户登录后势必会跳回购物车列表页,因此在查询list时合并一次即可。细节:合并前判断cookie中是否存在购物车数据,有则合并并清空cookie购物车,无则跳过。

控制层

修改pinyougou-cart-web工程CartController类的findCartList方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RequestMapping("/findCartList")
public List<Cart> findCartList(){
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String cartListString = util.CookieUtil.getCookieValue(request, "cartList", "UTF-8");
if(cartListString==null || cartListString.equals("")){
cartListString="[]";
}
List<Cart> cartList_cookie = JSON.parseArray(cartListString, Cart.class);
if(username.equals("anonymousUser")){//如果未登录
return cartList_cookie;
}else{
List<Cart> cartList_redis =cartService.findCartListFromRedis(username);//从redis中提取
if(cartList_cookie.size()>0){//如果本地存在购物车
//合并购物车
cartList_redis=cartService.mergeCartList(cartList_redis, cartList_cookie);
//清除本地cookie的数据
util.CookieUtil.deleteCookie(request, response, "cartList");
//将合并后的数据存入redis
cartService.saveCartListToRedis(username, cartList_redis);
}
return cartList_redis;
}
}

本文目标

  • 目标1:搭建单点登录服务端,开发单点登录客户端
  • 目标2:实现CAS 认证数据源设置
  • 目标3:更换CAS 登录页面
  • 目标4:掌握CAS与SpringSecurity集成
  • 目标5:完成用户中心单点登录功能

单点登录

  • 单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
  • 我们目前的系统存在诸多子系统,而这些子系统是分别部署在不同的服务器中,那么使用传统方式的session是无法解决的,我们需要使用相关的单点登录技术来解决。

CAS

什么是CAS

  • CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。CAS 具有以下特点:
    • 【1】开源企业级单点登录解决方案
    • 【2】CAS Server 为需要独立部署的 Web 应用
    • 【3】CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。

server和client

  • 从结构上看,CAS 包含两个部分: CAS Server 和CAS Client。
    • CAS Server :需要独立部署,主要负责对用户的认证工作;
    • CAS Client :负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server
  • 下图是CAS 最基本的协议过程:

sso流程

  • SSO单点登录访问流程主要有以下步骤:
  1. 访问服务:SSO客户端发送请求访问应用系统提供的服务资源。
  2. 定向认证:SSO客户端会重定向用户请求到SSO服务器。
  3. 用户认证:用户身份认证。
  4. 发放票据:SSO服务器会产生一个随机的Service Ticket。
  5. 验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。
  6. 传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。

CAS服务端部署

  • Cas服务端其实就是一个war包。在cas-server-4.0.0-release\cas-server-4.0.0\modules目录下cas-server-webapp-4.0.0.war ,将其改名为cas.war放入tomcat目录下的webapps下。启动tomcat自动解压war包。浏览器输入http://localhost:8080/cas/login , 可看到登录页面

我们使用CAS时是无需编码的,一切需要根据我们项目作出调整的需求都可更改相应的配置文件来实现。

登录认证

  • 默认提供的通行证账号为casuser,密码Mellon,按此登录方可登录成功。此通行逻辑源于webapps/cas/WEB-INF/deployerConfigContext.xml文件中的配置:
1
2
3
4
5
6
7
8
9
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
<property name="users">
<map>
<entry key="casuser" value="Mellon"/>
<entry key="tom" value="123"/>
</map>
</property>
</bean>

​ 你可以增加一个entry来增加一个通行证(如我增加了tom)。修改配置文件后别忘了重启tomcat以使其生效。

登出url

  • /logout为登出url,即请求localhost:8080/cas/logout即可注销当前浏览器在CAS中的登录。

端口修改

  • 如果我们不希望用8080端口访问CAS, 可以修改端口,需更改两处配置:

    • 打开tomcat 目录 conf\server.xml 找到下面的配置,更改端口号(如9100)
    1
    2
    3
    <Connector port="9100" protocol="HTTP/1.1"
    connectionTimeout="20000"
    redirectPort="8443" />
    • 修改CAS配置文件,修改webapps/cas/WEB-INF/cas.properties
    1
    server.name=http://localhost:9100

去除https协议

CAS默认使用的是HTTPS协议,如果使用HTTPS协议需要SSL安全证书(需向特定的机构申请和购买) 。如果对安全要求不高或是在开发测试阶段,可使用HTTP协议。我们这里讲解通过修改配置,让CAS使用HTTP协议。

  • 修改cas的``WEB-INF/deployerConfigContext.xml`,找到下面的配置
1
2
3
4
<!-- Required for proxy ticket mechanism. -->
<bean id="proxyAuthenticationHandler"
class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient" p:requireSecure="false"/>

​ 这里需要增加参数p:requireSecure="false",requireSecure属性意思为是否需要安全验证,即HTTPS,false为不采用

  • 修改cas的/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml,找到下面配置:
1
2
3
4
5
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="3600"
p:cookieName="CASTGC"
p:cookiePath="/cas" />

​ 参数p:cookieSecure="true",同理为HTTPS验证相关,TRUE为采用HTTPS验证,FALSE为不采用https验证。参数p:cookieMaxAge="-1",是cookie的最大生命周期,-1为无生命周期,即只在当前打开的窗口有效,关闭或重新打开其它窗口,仍会要求验证。可以根据需要修改为大于0的数字,比如3600等,意思是在3600秒内,打开任意窗口,都不需要验证。我们这里将cookieSecure改为false , cookieMaxAge 改为3600

  • 修改cas的WEB-INF/spring-configuration/warnCookieGenerator.xml,找到下面配置
1
2
3
4
5
<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="3600"
p:cookieName="CASPRIVACY"
p:cookiePath="/cas" />

​ 我们这里将cookieSecure改为false , cookieMaxAge 改为3600

CAS客户端入门小Demo

客户端工程1搭建

  1. 搭建工程引入依赖

    创建Maven工程 (war)casclient_demo1 引入cas客户端依赖并指定tomcat运行端口为9001

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    <dependencies>
    <!-- cas -->
    <dependency>
    <groupId>org.jasig.cas.client</groupId>
    <artifactId>cas-client-core</artifactId>
    <version>3.3.3</version>
    </dependency>
    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
    <scope>provided</scope>
    </dependency>
    </dependencies>
    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.3.2</version>
    <configuration>
    <source>1.7</source>
    <target>1.7</target>
    </configuration>
    </plugin>
    <plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <configuration>
    <!-- 指定端口 -->
    <port>9001</port>
    <!-- 请求路径 -->
    <path>/</path>
    </configuration>
    </plugin>
    </plugins>
    </build>
  2. 添加WEB-INF/web.xml,添加配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">

<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>

<!-- 该过滤器用于实现单点登出功能,可选配置。 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 该过滤器负责用户的认证工作,必须启用它 -->
<filter>
<filter-name>CASFilter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://localhost:9100/cas/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!-- cas客户端IP -->
<param-value>http://localhost:9001</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CASFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>http://localhost:9100/cas</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!-- cas客户端IP -->
<param-value>http://localhost:9001</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 -->
<filter>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
  1. 创建index.jsp
1
2
3
4
5
6
7
8
9
10
11
12
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>一品优购</title>
</head>
<body>
欢迎来到一品优购,<%=request.getRemoteUser()%>
</body>
</html>

request.getRemoteUser()为获取远程登录名

客户端工程2搭建

  • 创建Maven工程 (war)casclient_demo2 引入cas客户端依赖并制定tomcat运行端口为9002
  • 创建web.xml,参照casclient_demo1 ,将serverName(cas客户端IP)的值改为http://localhost:9002 , 一共两处
  • 创建index.jsp ,内容显示“欢迎来到品优购”

单点登录测试

  • 启动cas服务部署的tomcat(9100)
  • 启动客户端工程1和客户端工程2
  • 地址栏输入http://localhost:9001/http://localhost:9002/ ,地址均会跳转到CAS登录页
  • 输入用户名(casuser)和密码(Mellon)后,页面跳转回9002,再次访问9001也可以打开主页面。

单点登出

1
2
欢迎来到一品优购,<%=request.getRemoteUser()%>。
<a href="http://localhost:9100/cas/logout">点击退出</a>

登出到指定url

  • 我们更希望退出登录后,能自动跳转到某个页面,那如何处理呢?

    • 修改cas系统(9100)的配置文件WEB-INF/cas-servlet.xml中的${cas.logout.followServiceRedirects:true}
    1
    2
    3
    <bean id="logoutAction" class="org.jasig.cas.web.flow.LogoutAction"
    p:servicesManager-ref="servicesManager"
    p:followServiceRedirects="${cas.logout.followServiceRedirects:true}"/>
  • 这样在cas客户端的登出url中加上回调URL即可

1
<a href="http://localhost:9100/cas/logout?service=http://www.baidu.com">退出登录</a>

CAS服务端数据源设置

需求分析

  • 我们现在让用户名密码从数据库用户表tb_user里做验证

配置数据源

  • 修改cas服务端中WEB-INFdeployerConfigContext.xml ,在末尾</beans>前添加如下配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- c3p0数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
p:driverClass="com.mysql.jdbc.Driver"
p:jdbcUrl="jdbc:mysql://127.0.0.1:3306/pinyougoudb?characterEncoding=utf8"
p:user="root"
p:password="123456" />
<!-- md5加密 -->
<bean id="passwordEncoder"
class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder"
c:encodingAlgorithm="MD5"
p:characterEncoding="UTF-8" />

<!-- 数据库认证处理器 -->
<bean id="dbAuthHandler"
class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"
p:dataSource-ref="dataSource"
p:sql="select password from tb_user where username = ?"
p:passwordEncoder-ref="passwordEncoder"/>
  • 然后在该文件的开头部分找到如下配置:
1
2
3
4
5
6
7
<bean id="authenticationManager" class="org.jasig.cas.authentication.PolicyBasedAuthenticationManager">
<constructor-arg>
<map>
<entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" />
<entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />
</map>
</constructor-arg>
  • 其中<entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" />这一配置是使用固定的用户名和密码进行认证。就像我们之前配的tom,123

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <bean id="primaryAuthenticationHandler"
    class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
    <property name="users">
    <map>
    <entry key="casuser" value="Mellon"/>
    <entry key="tom" value="123"/>
    </map>
    </property>
    </bean>
  • 要想使用数据库表做动态认证,需要注释此行配置,改为我们添加的数据库认证处理器:

1
2
<1-- <entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" /> -->
<entry key-ref="dbAuthHandler" value-ref="primaryPrincipalResolver"/>

需要注意的是,我们添加的c3p0、md5加密、根据数据源进行认证的dbAuthHandler需要引入3个依赖到WEB-INF/lib

  • c3p0-0.9.1.2.jar
  • cas-server-support-jdbc-4.0.0.jar
  • mysql-connector-java-5.1.32.jar
  • 重启tomcat,使用数据库tb_user表中的数据做登录测试

CAS服务端界面改造

需求分析

  • 我们现在动手将CAS默认的登录页更改为自己项目的登陆页

改头换面

  • 拷贝资源
    • 品优购的登陆页login.html拷贝到cas系统下webapp\cas\WEB-INF\view\jsp\default\ui 目录下
    • css js等文件夹拷贝到 webapp/cas目录下
    • 将原来的casLoginView.jsp 改名为casLoginView-origin.jsp(留作参照),将login.html改名为casLoginView.jsp

修改页面

  • 修改casLoginView.jsp (参照casLoginView-origin.jsp

    • 添加jsp指令
    1
    2
    3
    4
    5
    6
    <%@ page pageEncoding="UTF-8" %>
    <%@ page contentType="text/html; charset=UTF-8" %>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
    <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
    • 修改form标签
    1
    2
    3
    <form:form method="post" id="fm1" commandName="${commandName}" htmlEscape="true" class="sui-form">
    ......
    </form:form>
    • 修改用户名框
    1
    2
    3
    <form:input id="username" tabindex="1" 
    accesskey="${userNameAccessKey}" path="username" autocomplete="off" htmlEscape="true"
    placeholder="邮箱/用户名/手机号" class="span2 input-xfat" />
    • 修改密码框
    1
    2
    3
    <form:password  id="password" tabindex="2" path="password" 
    accesskey="${passwordAccessKey}" htmlEscape="true" autocomplete="off"
    placeholder="请输入密码" class="span2 input-xfat" />
    • 修改登陆按钮
    1
    2
    3
    4
    <input type="hidden" name="lt" value="${loginTicket}" />
    <input type="hidden" name="execution" value="${flowExecutionKey}" />
    <input type="hidden" name="_eventId" value="submit" />
    <input class="sui-btn btn-block btn-xlarge btn-danger" accesskey="l" value="登陆" type="submit" />

错误提示

  • 在表单内加入登录失败提示框
1
<form:errors path="*" id="msg" cssClass="errors" element="div" htmlEscape="false" />
  • 测试:输入错误的用户名和密码,提示是英文。这个提示信息是在cas\WEB-INF\classes目录下的messages.properties文件中 :

    1
    2
    authenticationFailure.AccountNotFoundException=Invalid credentials.
    authenticationFailure.FailedLoginException=Invalid credentials.
  • 设置国际化为zn_CN ,修改WEB-INF\cas-servlet.xml

    1
    2
    3
    <bean id="localeResolver" 
    class="org.springframework.web.servlet.i18n.CookieLocaleResolver"
    p:defaultLocale="zh_CN" />
  • 我们在WEB-INF/classes/messages_zh_CN.properties下增加如下配置(中文转码为UTF-8):

    1
    2
    authenticationFailure.AccountNotFoundException=\u7528\u6237\u4E0D\u5B58\u5728.
    authenticationFailure.FailedLoginException=\u5BC6\u7801\u9519\u8BEF.

    第一个是用户名不存在时的错误提示

    第二个是密码错误的提示

CAS客户端与SpringSecurity集成

Spring Security测试工程搭建

  • (1)建立Maven项目casclient_demo3 ,引入spring依赖和spring secrity 相关依赖 ,tomcat端口设置为9003

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    <dependencies>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>${spring.version}</version>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>${spring.version}</version>
    </dependency>

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
    </dependency>

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${spring.version}</version>
    </dependency>

    <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>4.1.0.RELEASE</version>
    </dependency>

    <dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>4.1.0.RELEASE</version>
    </dependency>

    <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
    <scope>provided</scope>
    </dependency>

    <!-- cas -->
    <dependency>
    <groupId>org.jasig.cas.client</groupId>
    <artifactId>cas-client-core</artifactId>
    <version>3.3.3</version>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.apache.tomcat.maven</groupId>
    <artifactId>tomcat7-maven-plugin</artifactId>
    <configuration>
    <!-- 指定端口 -->
    <port>9003</port>
    <!-- 请求路径 -->
    <path>/</path>
    </configuration>
    </plugin>
    </plugins>
    </build>
  • (2)建立web.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/spring-security.xml</param-value>
    </context-param>
    <listener>
    <listener-class>
    org.springframework.web.context.ContextLoaderListener
    </listener-class>
    </listener>

    <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    </filter-mapping>
  • (3)创建配置文件spring-security.xml

    • spring-security.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <?xml version="1.0" encoding="UTF-8"?>
    <beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

    <!--
    拦截规则
    拦截哪些路径
    该用户需要什么角色才能访问该路径
    use-expressions="false"表示不启用SPEL表达式,如hasRole('ROLE_USER') -->
    <http use-expressions="false">
    <!--被授予ROLE_USER角色的用户才能访问根目录及所有其子目录下的资源-->
    <intercept-url pattern="/**" access="ROLE_ADMIN"></intercept-url>
    <form-login always-use-default-target="true" default-target-url="/index.html" login-page="/login.html" authentication-failure-url="/login.html" />
    <csrf disabled="true"/>
    <headers>
    <frame-options policy="SAMEORIGIN"/>
    </headers>
    <logout/>
    </http>

    <!-- 认证管理 -->
    <authentication-manager>
    <authentication-provider>
    <user-service>
    <user name="admin" authorities="ROLE_ADMIN" password="admin"/>
    </user-service>
    </authentication-provider>
    </authentication-manager>

    </beans:beans>
  • 添加/login.html/index.html测试SpringSecurity搭建是否成功

    • login.html
    1
    2
    3
    4
    5
    <form action="/login" method="post">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="submit" value="登录">
    </form>
    • index.html
    1
    欢迎光临

Spring Security与 CAS集成

  • (1)引入CAS客户端、SpringSecurity整合CAS的依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <dependency>  
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
    <version>4.1.0.RELEASE</version>
    </dependency>
    <dependency>
    <groupId>org.jasig.cas.client</groupId>
    <artifactId>cas-client-core</artifactId>
    <version>3.3.3</version>
    <exclusions>
    <exclusion>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
  • (2)修改spring-security.xml如下(把9001中web.xml中的配置移到了spring配置,并把SpringSecurity认证的提供指定为cas):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    <?xml version="1.0" encoding="UTF-8"?>
    <beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

    <!-- entry-point-ref 入口点引用 -->
    <http use-expressions="false" entry-point-ref="casProcessingFilterEntryPoint">
    <intercept-url pattern="/**" access="ROLE_USER"/>
    <csrf disabled="true"/>
    <!-- custom-filter为过滤器, position 表示将过滤器放在指定的位置上,before表示放在指定位置之前 ,after表示放在指定的位置之后
    CAS_FILTER为SpringSecurity内置过滤器的别名,详情参加本文末尾附录A -->
    <custom-filter ref="casAuthenticationFilter" position="CAS_FILTER" />
    <custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
    <custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
    </http>

    <!-- CAS入口点 开始 -->
    <beans:bean id="casProcessingFilterEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">

    <!-- 单点登录服务器登录URL -->
    <beans:property name="loginUrl" value="http://localhost:9100/cas/login"/>
    <beans:property name="serviceProperties" ref="serviceProperties"/>
    </beans:bean>

    <beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
    <!--service 配置自身工程的根地址+/login/cas,写法固定:根地址+/login/cas -->
    <beans:property name="service" value="http://localhost:9003/login/cas"/>
    </beans:bean>
    <!-- CAS入口点 结束 -->


    <!-- 认证过滤器 开始 -->
    <beans:bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
    <beans:property name="authenticationManager" ref="authenticationManager"/>
    </beans:bean>

    <!-- 认证管理器 -->
    <authentication-manager alias="authenticationManager">
    <authentication-provider ref="casAuthenticationProvider">
    </authentication-provider>
    </authentication-manager>

    <!-- 认证提供者 -->
    <beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">

    <!-- security认证类,这里只用来保存用户明细 -->
    <beans:property name="authenticationUserDetailsService">
    <beans:bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <beans:constructor-arg ref="userDetailsService" />
    </beans:bean>
    </beans:property>

    <!-- 由cas来做认证 -->
    <beans:property name="serviceProperties" ref="serviceProperties"/>

    <!-- ticketValidator 为票据验证器 -->
    <beans:property name="ticketValidator">
    <beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
    <beans:constructor-arg index="0" value="http://localhost:9100/cas"/>
    </beans:bean>
    </beans:property>
    <beans:property name="key" value="an_id_for_this_auth_provider_only"/>
    </beans:bean>

    <!-- 认证类,只是保存用户名,认证还是交由cas来做 -->
    <beans:bean id="userDetailsService" class="top.zhenganwen.demo.service.UserDetailServiceImpl"/>
    <!-- 认证过滤器 结束 -->


    <!-- 单点登出 开始 -->
    <beans:bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>
    <beans:bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">

    <beans:constructor-arg value="http://localhost:9100/cas/logout"/>

    <beans:constructor-arg>
    <beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
    </beans:constructor-arg>

    <beans:property name="filterProcessesUrl" value="/logout/cas"/>

    </beans:bean>
    <!-- 单点登出 结束 -->
    </beans:beans>
  • (3)创建UserDetailsServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 认证类
*/
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//拿用户名到数据库查询用户的事CAS系统帮你做了

//构建角色集合
List<GrantedAuthority> authorities=new ArrayList();
//TODO 应该从数据库查询该用户权限的,这里作为demo简化,直接赋值
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new User(username, "" , authorities);
}
}

这个类的主要作用是在登陆后得到用户名,可以根据用户名查询角色或执行一些逻辑。 如通过 SecurityContextHolder获取用户名。

  • (4)访问 localhost:9003 ,页面跳转到CAS服务端登录页面,输入数据库表中的用户登录成功后重定向到 localhost:9003,至此整合成功。

获取登录名

  • 我们在处理后端逻辑需要获得登录名,那么如何获取单点登录的用户名呢? 其实UserDetailServiceImpl已经帮我们保存了登录名,只需建立一个Controller并通过SecurityContextHolder获取返回给前端。

退出登录

  • 要想退出登录(http://localhost:9100/cas/logout)后调往指定页面,则需修改`spring-security.xml` ,如下面调往http://localhost:9003/index2.html:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ...
    <beans:bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
    <beans:constructor-arg value="http://localhost:9100/cas/logout?service=http://localhost:9003/index2.html"/>
    <beans:constructor-arg>
    <beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
    </beans:constructor-arg>

    <!-- 访问本工程的/logout/cas将单点退出cas系统 -->
    <beans:property name="filterProcessesUrl" value="/logout/cas"/>
    </beans:bean>
  • 在页面上添加链接,退出登录将跳往index2.html

    1
    <a href="/logout/cas">退出登录</a>
  • 创建index2.html,将index2.html设置为可匿名访问

    1
    <http pattern="/index2.html" security="none"></http>

附录A. Spring Security 内置过滤器表

别名 Filter
CHANNEL_FILTER ChannelProcessingFilter
SECURITY_CONTEXT_FILTER SecurityContextPersistenceFilter
CONCURRENT_SESSION_FILTER ConcurrentSessionFilter
LOGOUT_FILTER LogoutFilter
X509_FILTER X509AuthenticationFilter
PRE_AUTH_FILTER AstractPreAuthenticatedProcessingFilter 的子类
CAS_FILTER CasAuthenticationFilter
FORM_LOGIN_FILTER UsernamePasswordAuthenticationFilter
BASIC_AUTH_FILTER BasicAuthenticationFilter
SERVLET_API_SUPPORT_FILTER SecurityContextHolderAwareRequestFilter
JAAS_API_SUPPORT_FILTER JaasApiIntegrationFilter
REMEMBER_ME_FILTER RememberMeAuthenticationFilter
ANONYMOUS_FILTER AnonymousAuthenticationFilter
SESSION_MANAGEMENT_FILTER SessionManagementFilter
EXCEPTION_TRANSLATION_FILTER ExceptionTranslationFilter
FILTER_SECURITY_INTERCEPTOR FilterSecurityInterceptor
SWITCH_USER_FILTER SwitchUserFilter