Merikanto

一簫一劍平生意,負盡狂名十五年

User Login with Spring Boot

In this post, we will implement the User Login feature in Spring Boot. The dependencies we need are JDBC and MyBatis. After we are done with the backend logic, we will use a frontend template to bootstrap the page view.


Preparation

We will start the configuration based on the setup and codes in the previous post.


Project Structure

Before we start, the project structure looks like this (under /src/main/java/merikanto/demo):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
meirkanto.demo

├── common
│   ├── Constants.java
│   ├── ResultGenerator.java
│   └── Result.java
├── controller
│   └── UserController.java
├── dao
│   └── UserDao.java
├── entity
│   └── User.java
├── service
│   ├── impl
│   │   └── UserServiceImpl.java
│   └── UserService.java
└── utils
│ ├── DateUtil.java
│ ├── MD5Util.java
│ ├── NumberUtil.java
│ └── SystemUtil.java
└── DemoApplication.java

Define DB Schema

First thing is to define the database schema.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DROP DATABASE IF EXISTS demo_springboot;
CREATE DATABASE demo_springboot;
USE demo_springboot;

CREATE TABLE user (
id bigint(11) unsigned NOT NULL AUTO_INCREMENT,
username varchar(20) NOT NULL,
password_hash varchar(50) NOT NULL,
token varchar(50) NOT NULL,
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8;

INSERT INTO user (id, username, password_hash, token, create_time)
VALUES (1, 'merikanto','e10adc3949ba59abbe56e057f20f883e','6f1d93269e8bfdcd2066a248bfdafee6', '2020-03-25 12:00:00');

Backend Logic

DAO: Data Persistence

Note: application.properties and the main application Java file are the same.

User

Modify class User under the entity package.

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
package merikanto.demo.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {

private Long id; // id is the Primary Key

private String userName;

private String password;

private String userToken;


@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT-5")
private Date createTime;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

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;
}

public String getUserToken() {
return userToken;
}

public void setUserToken(String userToken) {
this.userToken = userToken;
}

public Date getCreateTime() {
return createTime;
}

public void setCreateTime(Date createTime) {
this.createTime = createTime;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", userName='" + userName + '\'' +
", password='" + password + '\'' +
", userToken='" + userToken + '\'' +
", createTime=" + createTime +
'}';
}
}

UserDao

Modify the class UserDao under the dao package.

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
package merikanto.demo.dao;

import merikanto.demo.entity.User;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;


public interface UserDao {

List<User> findUsers(Map param);

int getTotalUser(Map param);

User getUserByUserNameAndPassword(@Param("userName") String userName, @Param("passwordHash") String passwordHash);

User getUserByToken(String userToken);

User getUserById(Long id);

User getUserByUserName(String userName);

int addUser(User user);

int insertUsersBatch(@Param("Users") List<User> Users);

int updateUserPassword(@Param("userId") Long userId, @Param("newPassword") String newPassword);

int updateUserToken(@Param("userId") Long userId, @Param("newToken") String newToken);

int deleteBatch(Object[] ids);

List<User> getAllUsers();
}

UserDao.xml

Modify UserDao.xml under resources/mapper, add more complicated logic:

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
<?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="merikanto.demo.dao.UserDao">
<resultMap type="merikanto.demo.entity.User" id="UserResult">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="userName" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password_hash" jdbcType="VARCHAR"/>
<result property="userToken" column="token" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>

<insert id="addUser" parameterType="merikanto.demo.entity.User">
insert into user(username,password_hash)
values(#{userName},#{password})
</insert>

<insert id="insertUsersBatch">
insert into user(username,password_hash) VALUES
<foreach collection="Users" index="index" item="User" open="" separator="," close="">
(#{User.userName}, #{User.password})
</foreach>
</insert>

<select id="findUsers" parameterType="Map" resultMap="UserResult">
select id,username,create_time from user
order by id desc
<if test="start!=null and limit!=null">
limit #{start},#{limit}
</if>
</select>

<select id="getTotalUser" parameterType="Map" resultType="int">
select count(*) from user
</select>

<select id="getUserByUserNameAndPassword" resultMap="UserResult">
select id,username,token
from user
where username = #{userName} and password_hash = #{passwordHash}
ORDER BY id DESC limit 1
</select>

<select id="getUserByToken" resultMap="UserResult">
select id,username,token
from user
where token = #{userToken}
ORDER BY id DESC limit 1
</select>

<select id="getUserById" resultMap="UserResult">
select username,token
from user
where id=#{id}
ORDER BY id DESC limit 1
</select>

<select id="getUserByUserName" resultMap="UserResult">
select id,username,token
from user
where username = #{userName}
ORDER BY id DESC limit 1
</select>

<update id="updateUserToken">
update user set token = #{newToken} where id =#{userId}
</update>

<update id="updateUserPassword">
update user set password_hash = #{newPassword},user_token ='' where id =#{userId}
</update>

<delete id="deleteBatch">
delete from user where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>

<select id="getAllUsers" resultMap="UserResult">
select id,username,create_time from user
order by id desc
</select>
</mapper>

Service: Business Logic

UserService

Add class UserService under service package.

1
2
3
4
5
6
7
8
9
package merikanto.demo.service;

import merikanto.demo.entity.User;

public interface UserService {

// User Login
User updateTokenAndLogin(String userName, String password);
}

UserServiceImpl

Then add class UserServiceImpl under /service/impl.

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
package merikanto.demo.service.impl;

import merikanto.demo.dao.UserDao;
import merikanto.demo.entity.User;
import merikanto.demo.service.UserService;
import merikanto.demo.utils.MD5Util;
import merikanto.demo.utils.NumberUtil;
import merikanto.demo.utils.SystemUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service("UserService")
public class UserServiceImpl implements UserService {

@Autowired
private UserDao UserDao;

@Override
public User updateTokenAndLogin(String userName, String password) {
User User = UserDao.getUserByUserNameAndPassword(userName, MD5Util.MD5Encode(password, "UTF-8"));
if (User != null) {
//登录后即执行修改token的操作
String token = getNewToken(System.currentTimeMillis() + "", User.getId());

if (UserDao.updateUserToken(User.getId(), token) > 0) {
//返回数据时带上token
User.setUserToken(token);
return User;
}
}
return null;
}

// Get Token Value
private String getNewToken(String sessionId, Long userId) {
String src = sessionId + userId + NumberUtil.genRandomNum(4);
return SystemUtil.genToken(src);
}
}

Controller: Interaction with Frontend

UserController

Add class UserController under package controller.

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
package merikanto.demo.controller;

import merikanto.demo.common.Result;
import merikanto.demo.common.ResultGenerator;
import merikanto.demo.entity.User;
import merikanto.demo.service.UserService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/users")
public class UserController {

@Autowired
private UserService UserService;

@RequestMapping(value = "/login", method = RequestMethod.POST)
public Result login(@RequestBody User user) {
Result result = ResultGenerator.genFailResult("Oops... Login Failed!");
if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) {
result.setMessage("Please fill in the login info!");
}
User loginUser = UserService.updateTokenAndLogin(user.getUserName(), user.getPassword());
if (loginUser != null) {
result = ResultGenerator.genSuccessResult(loginUser);
}
return result;
}
}

Utilities

common

Constants

1
2
3
4
5
6
7
8
9
10
package merikanto.demo.common;

public class Constants {

public static final int RESULT_CODE_SUCCESS = 200; // Success
public static final int RESULT_CODE_BAD_REQUEST = 412; // Error
public static final int RESULT_CODE_NOT_LOGIN = 402; // Not logged in
public static final int RESULT_CODE_PARAM_ERROR = 406; // parameter error
public static final int RESULT_CODE_SERVER_ERROR = 500; // server error
}

Result

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
package merikanto.demo.common;

import java.io.Serializable;

public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int resultCode;
private String message;
private T data;

public Result() {
}

public Result(int resultCode, String message) {
this.resultCode = resultCode;
this.message = message;
}

public int getResultCode() {
return resultCode;
}

public void setResultCode(int resultCode) {
this.resultCode = resultCode;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public Result failure(String code) {
return new Result(500, "Server Error");
}

@Override
public String toString() {
return "Result{" +
"resultCode=" + resultCode +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}

ResultGenerator

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
package merikanto.demo.common;

import org.springframework.util.StringUtils;

public class ResultGenerator {
private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS";
private static final String DEFAULT_FAIL_MESSAGE = "FAIL";

public static Result genSuccessResult() {
Result result = new Result();
result.setResultCode(Constants.RESULT_CODE_SUCCESS);
result.setMessage(DEFAULT_SUCCESS_MESSAGE);
return result;
}

public static Result genSuccessResult(String message) {
Result result = new Result();
result.setResultCode(Constants.RESULT_CODE_SUCCESS);
result.setMessage(message);
return result;
}

public static Result genSuccessResult(Object data) {
Result result = new Result();
result.setResultCode(Constants.RESULT_CODE_SUCCESS);
result.setMessage(DEFAULT_SUCCESS_MESSAGE);
result.setData(data);
return result;
}

public static Result genFailResult(String message) {
Result result = new Result();
result.setResultCode(Constants.RESULT_CODE_SERVER_ERROR);
if (StringUtils.isEmpty(message)) {
result.setMessage(DEFAULT_FAIL_MESSAGE);
} else {
result.setMessage(message);
}
return result;
}

public static Result genNullResult(String message) {
Result result = new Result();
result.setResultCode(Constants.RESULT_CODE_BAD_REQUEST);
result.setMessage(message);
return result;
}

public static Result genErrorResult(int code, String message) {
Result result = new Result();
result.setResultCode(code);
result.setMessage(message);
return result;
}
}

utils

DateUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
package merikanto.demo.utils;

import java.text.SimpleDateFormat;
import java.util.Date;

// Format the dates
public class DateUtil {

public static String getDateString(Date date) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return formatter.format(date);
}
}

MD5Util

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
package merikanto.demo.utils;

import java.security.MessageDigest;

public class MD5Util {

private static String byteArrayToHexString(byte b[]) {
StringBuffer resultSb = new StringBuffer();
for (int i = 0; i < b.length; i++) {
resultSb.append(byteToHexString(b[i]));
}

return resultSb.toString();
}

private static String byteToHexString(byte b) {
int n = b;
if (n < 0) {
n += 256;
}
int d1 = n / 16;
int d2 = n % 16;
return hexDigits[d1] + hexDigits[d2];
}

public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname)) {
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
} else {
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
}
} catch (Exception exception) {
}
return resultString;
}

private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

NumberUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package merikanto.demo.utils;

public class NumberUtil {

private NumberUtil() {
}

// Generate random numbers of fixed length
public static int genRandomNum(int length) {
int num = 1;
double random = Math.random();
if (random < 0.1) {
random = random + 0.1;
}
for (int i = 0; i < length; i++) {
num = num * 10;
}
return (int) ((random * num));
}

}

SystemUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package merikanto.demo.utils;

import java.math.BigInteger;
import java.security.MessageDigest;

public class SystemUtil {

private SystemUtil(){}

public static String genToken(String src){
if (null == src || "".equals(src)){
return null;
}
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(src.getBytes());
return new BigInteger(1, md.digest()).toString(16);
} catch (Exception e) {
return null;
}
}

}

View Rendering

We choose AdminLTE for bootstrapping the view. The template can be downloaded online, so we won’t talk about it in detail here.

The public.js file under /resources/static/dist/js is here:

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
<!-- Regex start-->

// if is null
function isNull(obj) {
if (obj == null || obj == undefined || obj.trim() == "") {
return true;
}
return false;
}

// check length
function validLength(obj, length) {
if (obj.trim().length < length) {
return true;
}
return false;
}

// username
function validUserName(userName) {
var pattern = /^[a-zA-Z0-9_-]{4,16}$/;
if (pattern.test(userName.trim())) {
return (true);
} else {
return (false);
}
}

// User password check
function validPassword(password) {
var pattern = /^[a-zA-Z0-9]{6,20}$/;
if (pattern.test(password.trim())) {
return (true);
} else {
return (false);
}
}

<!-- Regex end-->


function login() {
var userName = $("#userName").val();
var password = $("#password").val();
if (isNull(userName)) {
showErrorInfo("Username Please!");
return;
}
if (!validUserName(userName)) {
showErrorInfo("Please input the right password!");
return;
}
if (isNull(password)) {
showErrorInfo("Password Please!");
return;
}
if (!validPassword(password)) {
showErrorInfo("Please input the right password!");
return;
}
var data = {"userName": userName, "password": password}
$.ajax({
type: "POST", //方法类型
dataType: "json", //预期服务器返回的数据类型
url: "users/login",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
success: function (result) {
if (result.resultCode == 200) {
$('.alert-danger').css("display", "none");
setCookie("token", result.data.userToken);
window.location.href = "/";
}
;
if (result.resultCode == 500) {
showErrorInfo("Please check your username and password.");
return;
}
},
error: function () {
$('.alert-danger').css("display", "none");
showErrorInfo("Interface error!");
return;
}
});
}


// ================== COOKIES =================== //

// write cookie
function setCookie(name, value) {
var Days = 30;
var exp = new Date();
exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path=/";

}

// read cookie
function getCookie(name) {
var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
if (arr = document.cookie.match(reg))
return unescape(arr[2]);
else
return null;
}

// Delete Cookie
function delCookie(name) {
var exp = new Date();
exp.setTime(exp.getTime() - 1);
var cval = getCookie(name);
if (cval != null)
document.cookie = name + "=" + cval + ";expires=" + exp.toGMTString();
}

// Check cookie
function checkCookie() {
if (getCookie("token") == null) {
swal("Not logged in", {
icon: "error",
});
window.location.href = "login.html";
}
}


function checkResultCode(code) {
if (code == 402) {
window.location.href = "login.html";
}
}


function showErrorInfo(info) {
$('.alert-danger').css("display", "block");
$('.alert-danger').html(info);
}

In localhost:8080, we should be able to see this:

And after we typed in the username and password, we will see the following notice:


And now we’ve successfully finished setting up the user login module.