spring boot從零搭建後臺基礎服務架構,包你看了不後悔
目前大多專案是前後端分離。在後臺介面服務開發過程中,往往我們需要先搭建一個基礎服務,比如登入註冊功能、自動對所有的介面進行token的安全校驗等,這樣可以防範安全問題的出現。並且這樣後續的同事可以只關注業務程式碼的開發,不需要關心基礎架構服務的實現。
這次我準備搭建一個簡單的後臺服務,用的是spring boot + mysql + jjwt + mybatis
。
1、搭建Spring boot專案
首先我們使用IDEA自帶的初始化專案功能,建立一個Spring boot專案,如圖:
或者線上生成,點選進入
pom.xml
<?xml version="1.0" encoding="UTF-8" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId >
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.zz</groupId>
<artifactId>rest-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name> rest-api</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
<scope>compile</scope>
</dependency>
<!-- Use MySQL Connector-J -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- image to base64 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<!-- jjwt支援 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!--commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>com.github.terran4j</groupId>
<artifactId>terran4j-commons-api2doc</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.10</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.15</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>3.15</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
複製程式碼
maven更改為國內阿里雲映象,這樣比較快
settings.xml:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>
複製程式碼
Mybatis 推薦外掛如下:
application.yml
# mysql
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: update
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
profiles:
active: dev
# 靜態資源配置
mvc:
static-path-pattern: /**
resources:
static-locations: file:/Users/wz/projects/blog/uploadFile/,classpath:/static/,classpath:/resources/,classpath:/file/,classpath:/templates/
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.zz.entity
#自定義
my:
tokenURL: "55555"
authURL: "88888"
複製程式碼
application-dev.yml
# mysql
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: ******
server:
port: 8080
複製程式碼
大概目錄結構如下
搭建細節不再贅述;
2、讀取自定義配置檔案
com.zz.config.MyConfiguration
package com.zz.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "my")
public class MyConfiguration {
private String tokenURL;
private String authURL;
public String getAuthURL() {
return this.authURL;
}
public void setAuthURL(String authURL) {
this.authURL = authURL;
}
public String getTokenURL() {
return this.tokenURL;
}
public void setTokenURL(String tokenURL) {
this.tokenURL = tokenURL;
}
}
複製程式碼
3、web服務配置
com.zz.config.MyConfiguration
package com.zz.config;
import com.zz.common.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 攔截所有請求,通過判斷是否有 @passToken 註解 決定是否需要跳過登入
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
複製程式碼
4、自定義返回統一的實體類Response
com.zz.model.Response
package com.zz.model;
public class Response {
private int code;
private String msg;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
複製程式碼
5、Utils公共方法類
com.zz.utils.HttpUtils
獲取Request、 Response、session
package com.zz.utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 獲取 Request 和 Response
*/
public class HttpUtils {
// 獲取 request
public static HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) return null;
return requestAttributes.getRequest();
}
// 獲取 response
public static HttpServletResponse getResponse() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) return null;
return requestAttributes.getResponse();
}
// 獲取 session
public static HttpSession getSession(){
HttpServletRequest request = getRequest();
if(request == null) return null;
return request.getSession();
}
}
複製程式碼
com.zz.utils.JWTUtils
JWT 生成token,驗證token
package com.zz.utils;
import com.zz.entity.User;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class JWTUtils {
// 生成簽名的時候使用的祕鑰secret
private static final String SECRETKEY = "KJHUhjjJYgYUllVbXhKDHXhkSyHjlNiVkYzWTBac1Yxkjhuad";
// expirationDate 生成jwt的有效期,單位秒
private static long expirationDate = 2 * 60 * 60;
/**
* 由字串生成加密key
*
* @return SecretKey
*/
private static SecretKey generalKey(String stringKey) {
byte[] encodedKey = Base64.decodeBase64(stringKey);
return new SecretKeySpec(encodedKey,0,encodedKey.length,"AES");
}
/**
* 建立 jwt
*
* @param user 登入成功後的使用者資訊
* @return jwt token
*/
public static String createToken(User user) {
// 指定簽名的時候使用的簽名演演算法,也就是header那部分,jwt已經將這部分內容封裝好了
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的時間
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 建立payload的私有宣告(根據特定的業務需要新增,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
Map<String,Object> claims = new HashMap<>();
claims.put("userId",user.getUserId());
claims.put("userName",user.getUserName());
claims.put("password",user.getPassword());
// 生成簽名的時候使用的祕鑰secret,這個方法本地封裝了的,一般可以從本地配置檔案中讀取,切記這個祕鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret,那就意味著客戶端是可以自我簽發jwt了
SecretKey key = generalKey(SECRETKEY + user.getPassword());
// 生成簽發人
// json形式字串或字串,增加使用者非敏感資訊儲存,如使用者id或使用者賬號,與token解析後進行對比,防止亂用
HashMap<String,Object> storeInfo = new HashMap<String,Object>();
storeInfo.put("userId",user.getUserId());
storeInfo.put("userName",user.getUserName());
String subject = storeInfo.toString();
// 下面就是在為payload新增各種標準宣告和私有宣告瞭
// 這裡其實就是new一個JwtBuilder,設定jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有宣告,一定要先設定這個自己建立的私有的宣告,這個是給builder的claim賦值,一旦寫在標準的宣告賦值之後,就是覆蓋了那些標準的宣告的
.setClaims(claims)
// 唯一隨機UUID
// 設定JWT ID:是JWT的唯一標識,根據業務需要,這個可以設定為一個不重複的值,主要用來作為一次性token,從而回避重放攻擊
.setId(UUID.randomUUID().toString())
// jwt的簽發時間
.setIssuedAt(now)
// 代表這個JWT的主體,即它的所有人,這個是一個json格式的字串,可以存放什麼userid,roldid之類的,作為什麼使用者的唯一標誌
.setSubject(subject)
// 設定簽名使用的簽名演演算法和簽名使用的祕鑰
.signWith(signatureAlgorithm,key);
if (expirationDate >= 0) {
long expMillis = nowMillis + expirationDate * 1000;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* 解密token,獲取宣告的實體
*
* @param token 加密後的token
* @return claims
*/
public static Claims parseToken(String token,User user) {
// 簽名祕鑰,和生成的簽名的祕鑰要保持一模一樣
SecretKey key = generalKey(SECRETKEY + user.getPassword());
// 獲取私有宣告
Claims claims = Jwts.parser()
// 設定簽名的祕鑰
.setSigningKey(key)
// 設定需要解析的token
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 校驗token
*
* @param token 加密後的token
* @param user 使用者資訊
* @return true|false
*/
public static Boolean verify(String token,User user) {
// 獲取私有宣告的實體
Claims claims = parseToken(token,user);
return claims.get("password").equals(user.getPassword());
}
}
複製程式碼
6、查詢實體類 query
所有的服務查詢都採用統一的各自的實體類
比如:
com.zz.query.UserQuery
使用者查詢實體
package com.zz.query;
public class UserQuery {
private String userName;
private String password;
private long userId;
private boolean showPassword;
public boolean isShowPassword() {
return showPassword;
}
public void setShowPassword(boolean showPassword) {
this.showPassword = showPassword;
}
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
複製程式碼
7、查詢後返回實體類
所有的服務查詢返回都採用統一的各自的實體類
比如:
com.zz.entity.User
使用者資料返回實體
package com.zz.entity;
public class User {
private long userId;
private String userName;
private String token;
private String password;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public long getUserId() {
return userId;
}
public void setUserId(long userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
複製程式碼
8 、介面實現三層架構
我們這採取的是三層架構:controller —> service —> mapper;
如果我們要寫一個User類介面,先宣告一個UserController路由控制層,然後這個裡呼叫UserService實現類方法,然後再呼叫mapper持久層去CRUD(mysql增查刪改)。
9、開始搭建註冊使用者功能
基礎搭建先暫停,開始實質業務的推演;
mysql的連線就不多說啦;
讓我們開始實現之旅吧;
com.zz.newController.UserController
使用者註冊
package com.zz.newController;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 登入
* author: wz
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/*
* @param userName
* @param password
* @return response
*/
@PostMapping("/add")
@PassToken
public Response addUser(@RequestParam String userName,@RequestParam String password,Response response) {
UserQuery query = new UserQuery();
User userData = null;
query.setUserName(userName);
query.setPassword(password);
int result;
String message = "";
// 判斷使用者是否已經存在
UserQuery findUserQuery = new UserQuery();
findUserQuery.setUserName(userName);
User existUser = this.userService.findUserByName(findUserQuery);
if (existUser == null) {
// 插入使用者
try {
result = this.userService.addUser(query);
message = "success";
} catch (Exception e) {
result = 0;
message = "error";
e.printStackTrace();
}
// 插入使用者成功後返回使用者資訊
if (result == 1) {
userData = this.userService.findUserByName(findUserQuery);
// 生成token
String token = null;
// 當前使用者
User currentUser = new User();
if (userData != null) {
currentUser.setUserId(userData.getUserId());
currentUser.setUserName(userData.getUserName());
currentUser.setPassword(password);
token = JWTUtils.createToken(currentUser);
}
if (token != null) {
userData.setToken(token);
// 獲取token使用者資訊
// Claims userDataFromToken = JWTUtils.parseToken(token,currentUser);
}
}
} else {
message = "使用者已經存在";
}
response.setData(userData);
response.setMsg(message);
return response;
}
}
複製程式碼
com.zz.service.UserService
Interface 使用者介面
package com.zz.service;
import com.zz.entity.User;
import com.zz.query.UserQuery;
import java.util.List;
import java.util.Map;
public interface UserService {
// 新增使用者
int addUser(UserQuery query);
}
複製程式碼
com.zz.service.impl.UserServiceImpl
使用者介面實現類
package com.zz.service.impl;
import com.zz.entity.User;
import com.zz.mapper.UserMapper;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public int addUser(UserQuery query){
return this.userMapper.insert(query);
}
}
複製程式碼
com.zz.mapper.UserMapper
mapper
package com.zz.mapper;
import com.zz.entity.User;
import com.zz.query.UserQuery;
import java.util.List;
public interface UserMapper {
int insert(UserQuery query);
}
複製程式碼
resources/mapper/UserMapper.xml
前後名字一定對應
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zz.mapper.UserMapper">
<resultMap id="BaseResult" type="com.zz.entity.User">
<id column="user_id" property="userId"></id>
<id column="user_name" property="userName"></id>
</resultMap>
<sql id="base">
user_id,user_name
<if test="showPassword">,password
</if>
</sql>
<sql id="base_condition">
<where>
<if test="userName!=null and userName!=''">
user_name=#{userName}
</if>
<if test="password!=null and password!=''">
and password=#{password}
</if>
</where>
</sql>
<insert id="insert">
INSERT INTO user(
user_name,password
) VALUES (
#{userName},#{password}
)
</insert>
</mapper>
複製程式碼
到此,整個介面書寫過程已全部完成,這就是在當前架構下寫一個介面的全部過程。
10、搭建web例項 ——註冊使用者
由於我們在配置檔案裡已經配置靜態資源的路徑,所以我們可以在resources裡面寫一個不分離的we b例項進行訪問。
resources/static/regist.html
註冊頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
<title>註冊使用者</title>
<!-- 引入樣式 -->
<link rel="stylesheet" href="css/regist.css"/>
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.1.3/weui.min.css">
</head>
<body>
<div class="container">
<div class="page form_page js_show">
<div class="weui-form">
<div class="weui-form__text-area">
<h2 class="weui-form__title">註冊新使用者</h2>
</div>
<div class="weui-form__control-area">
<div class="weui-cells__group weui-cells__group_form">
<div class="weui-cells weui-cells_form">
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">使用者名稱</label></div>
<div class="weui-cell__bd">
<input id="js_input——user" class="weui-input" placeholder="請輸入要設定的使用者名稱">
</div>
</div>
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">密碼</label></div>
<div class="weui-cell__bd">
<input id="js_input——pwd" type="password" class="weui-input" placeholder="請輸入要設定的密碼">
</div>
</div>
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">確認密碼</label></div>
<div class="weui-cell__bd">
<input id="js_input——pwd2" type="password" class="weui-input" placeholder="請再次輸入設定的密碼" type="number" pattern="[0-9]*">
</div>
</div>
</div>
</div>
</div>
<!-- <div class="weui-form__tips-area">-->
<!-- <p class="weui-form__tips">-->
<!-- 表單頁提示,居中對齊-->
<!-- </p>-->
<!-- </div>-->
<div class="weui-form__opr-area">
<a class="weui-btn weui-btn_primary" href="javascript:" id="submit">確定</a>
</div>
<div class="weui-form__extra-area">
<div class="weui-footer">
<!-- <p class="weui-footer__links">-->
<!-- <a href="javascript:void(0);" class="weui-footer__link">底部連結文字</a>-->
<!-- </p>-->
<p class="weui-footer__text">Copyright © 2019 alex wong</p>
</div>
</div>
</div>
<div id="js_toast" style="display: none;">
<div class="weui-mask_transparent"></div>
<div class="weui-toast">
<i class="weui-icon-success-no-circle weui-icon_toast"></i>
<p class="weui-toast__content">已完成</p>
</div>
</div>
</div>
</div>
</body>
<script src="js/md5.js"></script>
<script src="js/utils.js"></script>
<script src="js/dataService.js"></script>
<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.2.1/weui.min.js"></script>
<script src="js/regist.js"></script>
</html>
複製程式碼
static/js/dataService.js
const APIURL = '/';
window.dataService = {
//GET
get: (url,params = {}) => {
const searchArr = [];
Object.keys(params).forEach(n => {
searchArr.push(`${n}=${params[n]}`);
});
const searchStr = searchArr.length ? '?' + searchArr.join('&') : '';
const token = utils.getCookie('token');
return fetch(APIURL + url + searchStr,{
method: 'GET',headers: {
token
}
}).then(res => res.json());
},//POST
post: (url,params = {}) => {
const formData = new FormData();
Object.keys(params).forEach(n => {
formData.append(n,params[n]);
});
const token = utils.getCookie('token');
return fetch(APIURL + url,{
method: 'POST',headers: {
token
},body: formData
}).then(res => res.json());
},// 註冊
addUser(params) {
return this.post('user/add',params);
},// 登入
login(params) {
return this.post('user/login',// 使用者資訊
getUserInfo(params) {
return this.get('user/info',};
複製程式碼
static/js/utils.js
window.utils = {
// md5
generateMd5(userName,password) {
const salt = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p@!.";
const asciStr = userName + salt + password;
const asciArr = asciStr.split('');
const asciResult = [];
asciArr.forEach(n => {
asciResult.push(n.charCodeAt());
});
const ascireusltStr = asciResult.join(salt);
return hex_md5(ascireusltStr);
},// setCookie
setCookie(name,value) {
var time = 2 * 60 * 60 * 1000;
var exp = new Date();
exp.setTime(exp.getTime() + time);
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
},// getCookie
getCookie(name) {
var arr,reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
if (arr = document.cookie.match(reg))
return unescape(arr[2]);
else
return null;
}
};
複製程式碼
static/js/regist.js
// 獲取相關使用者資訊
const userNameInput = document.getElementById("js_input——user");
const passwordInput = document.getElementById("js_input——pwd");
const passwordConfirmInput = document.getElementById("js_input——pwd2");
const submitBtn = document.getElementById("submit");
// submit
submitBtn.onclick = () => {
const userName = userNameInput.value;
const password = passwordInput.value;
const confirmPassword = passwordConfirmInput.value;
// verify
if (!userName) {
weui.topTips('使用者姓名不能為空');
return;
} else if (!password) {
weui.topTips('使用者密碼不能為空');
return;
} else if (confirmPassword !== password) {
weui.topTips('前後密碼不一致,請重試');
return;
}
// 加密密碼
const newPassword = utils.generateMd5(userName,password);
// 註冊
dataService.addUser({
userName,password: newPassword,}).then(res => {
const {code,data,msg} = res;
if (!data) {
weui.topTips(msg);
} else {
weui.topTips(`註冊成功,歡迎 ${data.userName}`);
window.location.href = location.origin + '/login.html';
}
})
};
複製程式碼
效果如圖:
增加一些基本的校驗
使用者密碼加密傳輸,並驗證新使用者是否已經註冊
mysql 檢視下使用者表
11、後端-使用者登入功能
按上面第9步驟所述,下面的新增內容,請直接新增到上述服務中,不再全部展示程式碼。
com.zz.newController.UserController
首先要判斷使用者是否存在,如果存在,返回基本資訊並返回使用者憑證token
/**
* 登入
*
* @param userName 使用者名稱
* @param password 密碼
* @return {}
*/
@PostMapping("/login")
@PassToken
public Response login(@RequestParam String userName,Response response) {
UserQuery query = new UserQuery();
query.setUserName(userName);
query.setPassword(password);
// 驗證使用者和密碼
try {
// 判斷使用者是否已經存在
User existUser = this.userService.findUserByName(query);
// 生成token
String token = null;
// 當前使用者
User currentUser = new User();
if (existUser != null) {
currentUser.setUserId(existUser.getUserId());
currentUser.setUserName(existUser.getUserName());
currentUser.setPassword(password);
// 生成使用者憑證
token = JWTUtils.createToken(currentUser);
if (token != null) {
existUser.setToken(token);
}
response.setMsg("success");
response.setData(existUser);
} else {
// 登入失敗
response.setMsg("登入失敗,請檢查使用者名稱和密碼");
response.setData(null);
}
} catch (Exception e) {
response.setMsg("login failed");
response.setData(null);
e.printStackTrace();
}
return response;
}
複製程式碼
com.zz.service.UserService
package com.zz.service;
import com.zz.entity.User;
import com.zz.query.UserQuery;
import java.util.List;
import java.util.Map;
public interface UserService {
// 新增使用者
int addUser(UserQuery query);
//查詢單個使用者
User findUserById(UserQuery query);
User findUserByName(UserQuery query);
}
複製程式碼
com.zz.service.impl.UserServiceImpl
package com.zz.service.impl;
import com.zz.entity.User;
import com.zz.mapper.UserMapper;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public int addUser(UserQuery query){
return this.userMapper.insert(query);
}
@Override
public User findUserById(UserQuery query) {
return this.userMapper.findUserById(query);
}
@Override
public User findUserByName(UserQuery query) {
return this.userMapper.findUserByName(query);
}
@Override
public List<User> findAllUser(UserQuery query) {
return this.userMapper.findAllUser(query);
}
}
複製程式碼
com.zz.mapper.UserMapper
package com.zz.mapper;
import com.zz.entity.User;
import com.zz.query.UserQuery;
import java.util.List;
public interface UserMapper {
int insert(UserQuery query);
User findUserById(UserQuery query);
User findUserByName(UserQuery query);
List<User> findAllUser(UserQuery query);
}
複製程式碼
mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zz.mapper.UserMapper">
<resultMap id="BaseResult" type="com.zz.entity.User">
<id column="user_id" property="userId"></id>
<id column="user_name" property="userName"></id>
</resultMap>
<sql id="base">
user_id,user_name
<if test="showPassword">,password
</if>
</sql>
<sql id="base_condition">
<where>
<if test="userName!=null and userName!=''">
user_name=#{userName}
</if>
<if test="password!=null and password!=''">
and password=#{password}
</if>
</where>
</sql>
<!-- 查詢所有user -->
<select id="findAllUser" resultMap="BaseResult">
select
<include refid="base"/>
from user
</select>
<!-- 查詢user -->
<select id="findUserById" resultMap="BaseResult">
select
<include refid="base"/>
from user
where
user_id = #{userId}
</select>
<select id="findUserByName" resultMap="BaseResult">
select
<include refid="base"/>
from user
<include refid="base_condition"/>
</select>
<insert id="insert">
INSERT INTO user(
user_name,#{password}
)
</insert>
</mapper>
複製程式碼
12、搭建web例項 ——登入使用者
static/login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,user-scalable=0">
<title>login</title>
<!-- 引入樣式 -->
<link rel="stylesheet" href="css/regist.css"/>
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.1.3/weui.min.css">
</head>
<body>
<div class="container">
<div class="page form_page js_show">
<div class="weui-form">
<div class="weui-form__text-area">
<h2 class="weui-form__title">登入</h2>
</div>
<div class="weui-form__control-area">
<div class="weui-cells__group weui-cells__group_form">
<div class="weui-cells weui-cells_form">
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">使用者名稱</label></div>
<div class="weui-cell__bd">
<input id="js_input——user" class="weui-input" placeholder="請輸入使用者名稱">
</div>
</div>
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">密碼</label></div>
<div class="weui-cell__bd">
<input id="js_input——pwd" type="password" class="weui-input" placeholder="請輸入密碼">
</div>
</div>
</div>
</div>
</div>
<!-- <div class="weui-form__tips-area">-->
<!-- <p class="weui-form__tips">-->
<!-- 表單頁提示,居中對齊-->
<!-- </p>-->
<!-- </div>-->
<div class="weui-form__opr-area">
<a class="weui-btn weui-btn_primary" href="javascript:" id="submit">確定</a>
</div>
<div class="weui-form__extra-area">
<div class="weui-footer">
<!-- <p class="weui-footer__links">-->
<!-- <a href="javascript:void(0);" class="weui-footer__link">底部連結文字</a>-->
<!-- </p>-->
<p class="weui-footer__text">Copyright © 2019 alex wong</p>
</div>
</div>
</div>
<div id="js_toast" style="display: none;">
<div class="weui-mask_transparent"></div>
<div class="weui-toast">
<i class="weui-icon-success-no-circle weui-icon_toast"></i>
<p class="weui-toast__content">已完成</p>
</div>
</div>
</div>
</div>
</body>
<script src="js/md5.js"></script>
<script src="js/utils.js"></script>
<script src="js/dataService.js"></script>
<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.2.1/weui.min.js"></script>
<script src="js/login.js"></script>
</html>
複製程式碼
static/js/login.js
// 獲取相關使用者資訊
const userNameInput = document.getElementById("js_input——user");
const passwordInput = document.getElementById("js_input——pwd");
const submitBtn = document.getElementById("submit");
// submit
submitBtn.onclick = () => {
const userName = userNameInput.value;
const password = passwordInput.value;
// verify
if (!userName) {
weui.topTips('使用者姓名不能為空');
return;
} else if (!password) {
weui.topTips('使用者密碼不能為空');
return;
}
// 加密密碼
const newPassword = utils.generateMd5(userName,password);
// 註冊
dataService.login({
userName,msg} = res;
if (!data) {
weui.topTips(msg);
} else {
weui.topTips(`登入成功,歡迎 ${data.userName}`);
utils.setCookie('token',data.token);
location.href = location.origin + '/home.html';
}
})
};
複製程式碼
登入介面返回使用者憑證token,後續用來校驗使用者介面,增加安全性。
13、增加自定義註解和攔截器
在常規的業務開發中,切記不可把介面服務暴露給任何人都可以訪問,不然別人可以任意檢視或者修改你的資料,這是很嚴重的事情。除了常規從網段IP方面限制固定客戶端IP的範圍,介面本身也要增加安全驗證,這個時候我們就需要用到之前生成的使用者憑證token;
問題是我們如果自定義控制,哪些介面是需要經過驗證,哪些介面是不需要通過驗證的呢?有人可能會說,直接全部驗證不就可以了,何苦糾結。但是在真實的業務中,有些介面是不能強制校驗的,比如一些使用者分享到微信的那種介面,是不能增加驗證,否則分享的頁面無法正常顯示。
所以我們可以自定義註解@PassToken,新增這個註解的介面,就可以不用進行token驗證了。
com.zz.common.annotation.PassToken
PassToken 註解
package com.zz.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 是否跳過token驗證
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
複製程式碼
新增攔截器
com.zz.common.interceptor.AuthenticationInterceptor
在傳送請求的時候,在請求頭裡面加token,然後驗證的時候token從頭部獲取
如果沒有token,進行無token提示;
如果存在,就用 JWT 校驗 token 是否存在,並且校驗使用者密碼是否正確。
package com.zz.common.interceptor;
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.mongodb.util.JSON;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
// 攔截器
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
/**
* response返回資訊
*
* @param code
* @param message
* @return
* @throws JSONException
*/
public JSONObject getJsonObject(int code,String message) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg",message);
jsonObject.put("code",code);
return jsonObject;
}
@Override
public boolean preHandle(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Object object) throws Exception {
// 從 http 請求頭中取出 token
String token = BaseApplicationController.getToken();
// 如果不是對映到方法直接通過
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//檢查是否有PassToken註釋,有則跳過認證
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
// 預設執行認證
httpServletResponse.setContentType("application/json;charset=UTF-8");
if (token == null || token.equals("null")) {
JSONObject jsonObject = getJsonObject(403,"無token,請重新登入");
httpServletResponse.getWriter().write(jsonObject.toString());
return false;
// throw new RuntimeException("無token,請重新登入");
}
// 獲取 token 中的 user id
long userId;
try {
userId = BaseApplicationController.getCurrentUserId();
} catch (JWTDecodeException j) {
JSONObject jsonObject = getJsonObject(500,"訪問異常,token不正確,請重新登入");
httpServletResponse.getWriter().write(jsonObject.toString());
return false;
// throw new RuntimeException("訪問異常!");
}
// 驗證使用者是否存在
UserQuery query = new UserQuery();
query.setUserId(userId);
query.setShowPassword(Boolean.TRUE);
User user = userService.findUserById(query);
if (user == null) {
JSONObject jsonObject = getJsonObject(500,"使用者不存在,請重新登入");
httpServletResponse.getWriter().write(jsonObject.toString());
return false;
// throw new RuntimeException("使用者不存在,請重新登入");
}
// 驗證token是否有效
Boolean verify = JWTUtils.verify(token,user);
if (!verify) {
JSONObject jsonObject = getJsonObject(500,"非法訪問,請重新登入");
httpServletResponse.getWriter().write(jsonObject.toString());
return false;
// throw new RuntimeException("非法訪問!");
}
return true;
}
}
複製程式碼
下面讓我們例項看下效果:
com.zz.newController.UserController
package com.zz.newController;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 登入
* autho: alex wong
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/*
* @param userName
* @param password
* @return response
*/
@PostMapping("/add")
@PassToken
public Response addUser(@RequestParam String userName,currentUser);
}
}
} else {
message = "使用者已經存在";
}
response.setData(userData);
response.setMsg(message);
return response;
}
/**
* 登入
*
* @param userName 使用者名稱
* @param password 密碼
* @return {}
*/
@PostMapping("/login")
@PassToken
public Response login(@RequestParam String userName,Response response) {
UserQuery query = new UserQuery();
query.setUserName(userName);
query.setPassword(password);
// 驗證使用者和密碼
try {
// 判斷使用者是否已經存在
User existUser = this.userService.findUserByName(query);
// 生成token
String token = null;
// 當前使用者
User currentUser = new User();
if (existUser != null) {
currentUser.setUserId(existUser.getUserId());
currentUser.setUserName(existUser.getUserName());
currentUser.setPassword(password);
token = JWTUtils.createToken(currentUser);
if (token != null) {
existUser.setToken(token);
}
response.setMsg("success");
response.setData(existUser);
} else {
// 登入失敗
response.setMsg("登入失敗,請檢查使用者名稱和密碼");
response.setData(null);
}
} catch (Exception e) {
response.setMsg("login failed");
response.setData(null);
e.printStackTrace();
}
return response;
}
/**
* 獲取個人資訊
*
* @return {}
*/
@GetMapping("/info")
public Response getUserInfo(Response response) {
// 獲取token
String token = BaseApplicationController.getToken();
User userData2 = BaseApplicationController.getCurrentUser();
Map<String,Object> headerData = BaseApplicationController.getHeader();
if (token != null && !token.equals("null")) {
User userData = new User();
DecodedJWT claims = JWT.decode(token);
userData.setUserName(claims.getClaim("userName").asString());
userData.setUserId(claims.getClaim("userId").asLong());
response.setData(userData);
response.setMsg("success");
} else {
response.setMsg("token不存在");
}
return response;
}
}
複製程式碼
我們新寫了一個介面,獲取使用者資訊,如上,其餘程式碼不再贅述;
成功獲取使用者資訊
刪除token
Token 故意改錯
到此,驗證過程完美成功;
14、總結
以上過程,只是一個簡單服務的搭建,真實的服務還需要更多配置,比如XSS配置防止XSS攻擊,多源資料等。好了,本篇文章到此為止,如果有哪裡描述得不清楚,請多多包涵。