服务器端使用springboot框架。
一、需要添加的pom依赖
本实例没有什么特殊的依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
二、yml配置文件。
只是端口设置。
server:
port: 7021
三、跨域设置文件CorsConfig.java
package com.chris.sl.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 跨域访问设置
*/
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); // 1允许任何域名使用
corsConfiguration.addAllowedHeader("*"); // 2允许任何头
corsConfiguration.addAllowedMethod("*"); // 3允许任何方法(post、get等)
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig()); // 4
return new CorsFilter(source);
}
}
四、需要用到的实例类
1. 用于传输登录信息的LoginUser.java
package com.chris.sl.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* create by: Chris Chan
* create on: 2019/10/2 11:31
* use for: 登录用户
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser {
private String username;
private String password;
}
2. 用于存放扫码登录信息的LoginInfo.java
package com.chris.sl.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* create by: Chris Chan
* create on: 2019/10/2 11:34
* use for: 登录信息
* 包括登录用户名和识别码过期时间
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfo {
private String username;
private long expire;
//是否已被占用
private boolean occ;
}
3. 用于给前端包装返回识别码的ScanCode.java
package com.chris.sl.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* create by: Chris Chan
* create on: 2019/10/2 13:13
* use for:
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ScanCode {
private String code;
}
五、数据管理器DataManager.java,类似于数据库的管理。本文使用内存操作。实际业务可能需要操作redis等数据库,改变一下各个方法的具体实现即可。
package com.chris.sl.manager;
import com.chris.sl.model.LoginInfo;
import com.chris.sl.model.LoginUser;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* create by: Chris Chan
* create on: 2019/10/2 11:28
* use for: 数据管理器
* 因为目的只是为了走通逻辑,为方便移植,此处不用外部数据库,用内存数据模拟数据库
* 如果改用mysql、redis,只需要改变方法的实现
*/
public class DataManager {
//用户信息
public static Map<String, LoginUser> userMap = new HashMap<>(16);
//登录识别码池
public static Map<String, LoginInfo> loginInfoMap = new HashMap<>(16);
/**
* 初始化
*/
public static void init() {
userMap.put("kaly", new LoginUser("kaly", "123456"));
userMap.put("chris", new LoginUser("chris", "123456"));
userMap.put("will", new LoginUser("will", "123456"));
userMap.put("chenfabao", new LoginUser("chenfabao", "123456"));
}
/**
* 添加一个登录信息
*
* @param code
*/
public static void addLoginInfo(String code) {
//过期时间设置为5分钟
long exp = LocalDateTime.now().plusMinutes(5).toInstant(ZoneOffset.of("+8")).toEpochMilli();
loginInfoMap.put(code, new LoginInfo(null, exp, false));
}
/**
* 占用登录信息
*
* @param code
* @param username
*/
public static void occLoginInfo(String code, String username) {
LoginInfo loginInfo = loginInfoMap.get(code);
if (null == loginInfo) {
return;
}
loginInfo.setUsername(username);
loginInfo.setOcc(true);
}
/**
* 验证用户
*
* @param username
* @param password
* @param code
* @return
*/
public static boolean verification(String username, String password, String code) {
LoginUser loginUser = userMap.get(username);
//检查用户是否存在
if (loginUser == null) {
return false;
}
//检查密码是否正确
if (!password.equalsIgnoreCase(loginUser.getPassword())) {
return false;
}
//检查二维码识别码是否存在或过期
LoginInfo loginInfo = loginInfoMap.get(code);
long currentTime = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
if (null == loginInfo || currentTime > loginInfo.getExpire()) {
return false;
}
return true;
}
/**
* 检查占用
*
* @param code
* @return
*/
public static boolean checkOcc(String code) {
refreshLoginInfo();//整理
LoginInfo loginInfo = loginInfoMap.get(code);
if (null == loginInfo) {
return false;
}
return loginInfo.isOcc();
}
/**
* 获取登录用户名
*
* @param code
* @return
*/
public static LoginUser getUserByCode(String code) {
LoginInfo loginInfo = loginInfoMap.get(code);
if (null == loginInfo) {
return null;
}
return userMap.get(loginInfo.getUsername());
}
/**
* 整理登录信息
* 主要是把已经过期的部分全部删除,否则数据会很大
*/
public static void refreshLoginInfo() {
//收集过期的code
Set<String> codeSet = new HashSet<>(16);
//收集
long currentTime = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
for (Map.Entry<String, LoginInfo> entry : loginInfoMap.entrySet()) {
if (currentTime > entry.getValue().getExpire()) {
codeSet.add(entry.getKey());
}
}
//删除
for (String code : codeSet) {
loginInfoMap.remove(code);
}
}
}
六、接口层UserApi.java,主要就是三个接口
package com.chris.sl.api;
import com.chris.sl.manager.DataManager;
import com.chris.sl.model.LoginUser;
import com.chris.sl.model.ScanCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
/**
* create by: Chris Chan
* create on: 2019/10/2 11:11
* use for: 接口
*/
@RestController
@RequestMapping("/api/user")
public class UserApi {
/**
* 登录 App调用
* App扫描二维码之后,获得登录识别码,然后携带自己的帐号密码一起调用登录
* 服务器在后台对三个信息进行验证,验证成功后把帐号放进登录呼叫池,等待前端下一次轮询
*
* @param username
* @param password
* @param code
* @return
*/
@GetMapping("/login")
public Boolean login(String username, String password, String code) {
//验证
if (DataManager.verification(username, password, code)) {
//占用
DataManager.occLoginInfo(code, username);
return true;
}
System.out.println("验证失败");
return false;
}
/**
* 获取扫描识别码 Web前端调用
* 用于实现一个二维码
* 同时服务器将这个二维码保存在数据库,同时设置好过期时间
*
* @return
*/
@GetMapping("/getScanCode")
public ScanCode getScanCode() {
String code = UUID.randomUUID().toString();
//添加到数据库
DataManager.addLoginInfo(code);
return new ScanCode(code);
}
/**
* 询问登录 Web前端轮询
* 1秒一次,后端根据code查看数据库,是否由用户绑定此code且通过了验证,等待登录
* 如果成功,将用户名返回给前端
*
* @param code
* @return
*/
@GetMapping("/askLogin")
public LoginUser askLogin(String code) {
System.out.println(code);
if (DataManager.checkOcc(code)) {
return DataManager.getUserByCode(code);
}
return null;
}
}