这个Demo用SpringBoot实现一个小小的登录功能,同时这个登录功能没有使用Session,用token代替,实现了服务器的无状态。
主要参考了下面4篇文章,
一、创建SpringBoot项目,实现简单的页面访问功能
1.创建一个简单的Maven项目
2.修改pom文件,添加web组件和Freemarker组件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
3.创建控制器,Freemarker页面和启动类
IndexController.java
package org.mkh.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
/**
*
* @author mo
* @date 2019/06/21
*/
@Controller
public class IndexController {
@GetMapping("/index")
public String index(Model model) {
return "index";
}
}
index.ftl
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
Welcome!
</body>
</html>
App.java
package org.mkh;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
/**
*
* @author mo
* @date 2019/06/20
*/
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
4.测试运行通过,demo框架搭建完毕。
二、实现User的业务逻辑
本Demo只做一个模拟登录功能,所以不做数据持久化,用HashMap存放数据代替。
1.创建实体类User
package org.mkh.entity;
public class User {
private Integer id;
private String username;
private String nickname;
private String password;
public User() {}
public User(Integer id, String username, String nickname, String password) {
this.id = id;
this.username = username;
this.nickname = nickname;
this.password = password;
}
@Override
public String toString() {
return "User [id=" + id + ", username=" + username + ", nickname=" + nickname + ", password=" + password + "]";
}
//省略Getter,Setter
}
2.定义UserServive接口,提供3个方法
package org.mkh.service;
import java.util.List;
import org.mkh.entity.User;
public interface UserService {
// 通过username获取用户
User getUserByUsername(String username);
// 通过userId获取用户
User getUserbyId(Integer userId);
// 查找所有用户
List<User> findAllUser();
}
3.实现UserService接口
package org.mkh.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mkh.entity.User;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
private static Map<String, User> users = new HashMap<>();
static {
users.put("zs", new User(99991001, "zs", "张三", "zs123"));
users.put("ls", new User(99991002, "ls", "李四", "ls123"));
users.put("ww", new User(99991003, "ww", "王五", "ww123"));
users.put("zl", new User(99991004, "zl", "赵六", "zl123"));
users.put("tq", new User(99991005, "tq", "田七", "tq123"));
}
@Override
public User getUserByUsername(String username) {
return users.get(username);
}
@Override
public List<User> findAllUser() {
return new ArrayList<User>(users.values());
}
@Override
public User getUserbyId(Integer userId) {
for (String key : users.keySet()) {
User user = users.get(key);
if (user.getId().intValue() == userId) {
return user;
}
}
return null;
}
}
4.实现UserController
package org.mkh.controller;
import org.mkh.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public String users(Model model) {
model.addAttribute("users", userService.findAllUser());
return "user-list";
}
}
5.实现user-list页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
<table border="1" cellspacing="0" cellpadding="0" width="500" align="center">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Nickname</th>
<th>Password</th>
</tr>
</thead>
<tbody>
<#list users as user>
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.nickname}</td>
<td>${user.password}</td>
</tr>
</#list>
</tbody>
</table>
</body>
</html>
6.启动项目,查看用户列表
三、实现简单的登录
1.实现登录控制器,登录成功跳转index页面,失败跳转signin-error页面
package org.mkh.controller;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.mkh.entity.User;
import org.mkh.service.UserService;
import org.mkh.util.CookieUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
/**
*
* @author mo
* @date 2019/06/20
*/
@Controller
public class SigninController {
@Autowired
private UserService userService;
@GetMapping("/signin")
public String signInput() {
return "signin";
}
@PostMapping("/signin")
public String sign(String username, String password, HttpServletResponse resp) {
System.out.println(username + "-" + password);
User user = userService.getUserByUsername(username);
if (user != null && user.getPassword().equals(password)) {
return "redirect:/index";
}
return "signin-error";
}
}
2.signin页面
<html>
<head>
<meta charset="utf-8">
<title>sign</title>
</head>
<body>
<form action="/signin" method="post">
Username:<input type="text" name="username" /><br>
Password:<input type="password" name="password" /><br>
<input type="submit" value="submit"/>
</form>
</body>
</html>
3.signin-error页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
Login failed! username incorrected or password incorrected!
</body>
</html>
至此,已完成了一个简单的登录功能,但是这个登录功能非常简陋,没有过滤器,没有存Session等等。
四、实现无状态的登录
所谓无状态就是服务器不记录用户的会话,每个请求对于服务器来说都是一个新的会话。HTTP协议是一个无状态的协议。无状态的关键在于取消Session,用token代替。服务器只负责生成token,发放token和验证token的合法性,服务器并不存储token,token交给客户端存储,客户端每发一个请求都会把token发送到服务器。
原理部分,大家可以看看上面的4篇文章。下面开始实现。
1.编写工具类ApplicationContextUtil
,用于把Spring容器管理的Bean注入到普通Java类中
package org.mkh.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
*
* @author mo
* @date 2019/06/21
*/
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return context;
}
public static Object getBean(String name) {
return context.getBean(name);
}
public static <T> T getBean(Class<T> clz) {
return context.getBean(clz);
}
public static <T> T getBean(String name, Class<T> clz) {
return context.getBean(name, clz);
}
}
2.编写工具类CookieUtil
,用于生成Cookie
和验证Cookie
的合法性
package org.mkh.util;
import java.time.Instant;
import javax.servlet.http.Cookie;
import org.mkh.auth.LocalCookieAuthenticator;
import org.mkh.entity.User;
import org.springframework.util.DigestUtils;
public class CookieUtil {
// 服务器密钥
private static final String SECRET_KEY = "secret";
// Cookie的组成部分的数目
private static final short PART_NUM = 3;
/**
* 生成cookie
*
* @param user
* @return
*/
public static Cookie generateCookie(User user) {
int uid = user.getId();
String password = user.getPassword();
long expires = Instant.now().plusSeconds(60).toEpochMilli(); // 60秒过期
String value = uid + ":" + password + ":" + expires + ":" + SECRET_KEY;
// 使用MD5加密,生成签名
String signature = DigestUtils.md5DigestAsHex(value.getBytes());
String cookieVal = uid + ":" + expires + ":" + signature;
return new Cookie(LocalCookieAuthenticator.USER_AUTH_COOKIE, cookieVal);
}
/**
* 验证Cookie合法性
*
* @param cookie
* @return
*/
public static String[] splitCookie(String cookie) {
String[] values = cookie.split(":");
if (values.length != PART_NUM) {
System.out.println("cookie不合法,可能被篡改");
return null;
}
for (String v : values) {
if (v == null || "".equals(v)) {
System.out.println("cookie数据缺失");
return null;
}
}
return values;
}
/**
* 验证cookie是否过期
*
* @param expires
* @return
*/
public static boolean validateCookieExpire(long expires) {
if (expires <= System.currentTimeMillis()) {
System.out.println("cookie 已过期");
return false;
}
return true;
}
public static boolean validateCookieSignature(int userId, long expires, User user, String clientSignature) {
// 用客户端传过来的数据生成签名
String value = userId + ":" + user.getPassword() + ":" + expires + ":" + SECRET_KEY;
String serverSignature = DigestUtils.md5DigestAsHex(value.getBytes());
System.out.println("server:" + serverSignature);
System.out.println("client:" + clientSignature);
// 比较用户上传的签名与生成的签名是否一样
if (serverSignature.equals(clientSignature)) {
System.out.println("cookie验证成功");
return true;
}
return false;
}
}
3.定义一个认证的接口,认证成功返回认证用户。可以有多种不同的认证方式,实现该接口即可
package org.mkh.auth;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.mkh.entity.User;
import org.mkh.ex.AuthenticateException;
public interface Authenticator {
// 认证成功返回User,认证失败抛出异常,无认证信息返回null:
User authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticateException;
}
4.实现上面的接口,用Cookie来认证
package org.mkh.auth;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.mkh.entity.User;
import org.mkh.ex.AuthenticateException;
import org.mkh.service.UserService;
import org.mkh.util.ApplicationContextUtil;
import org.mkh.util.CookieUtil;
public class LocalCookieAuthenticator implements Authenticator {
public static final String USER_AUTH_COOKIE = "UserInfo";
@Override
public User authenticate(HttpServletRequest request, HttpServletResponse response) throws AuthenticateException {
String cookie = getCookieFromRequest(request, USER_AUTH_COOKIE);
if (cookie == null) {
return null;
}
return getUserByCookie(cookie);
}
// 从request中获取指定名称的Cookie
private String getCookieFromRequest(HttpServletRequest request, String cookieName) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null;
}
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
return null;
}
/**
* 通过cookie查找用户
*
* @param cookie
* @return
*/
private User getUserByCookie(String cookie) {
// 验证cookie的格式是否符合规范
String[] values = CookieUtil.splitCookie(cookie);
if (values == null) {
throw new AuthenticateException(10001, "认证失败");
}
// 验证数据的合法性
Integer userId = Integer.parseInt(values[0]);
long expires = Long.parseLong(values[1]);
String clientSignature = values[2];
// cookie 过期
if (!CookieUtil.validateCookieExpire(expires)) {
throw new AuthenticateException(10002, "会话过期");
}
// 获取用户
// 通过ApplicationContextUtil注入userService
UserService userService = ApplicationContextUtil.getBean(UserService.class);
User user = userService.getUserbyId(userId);
if (user == null) {
System.out.println("没有此用户");
throw new AuthenticateException(10003, "认证失败");
}
// 验证签名
if (!CookieUtil.validateCookieSignature(userId, expires, user, clientSignature)) {
System.out.println("cookie验证失败");
throw new AuthenticateException(10004, "认证失败");
}
return user;
}
}
5.实现SigninFilter拦截未认证用户的访问
package org.mkh.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.mkh.auth.Authenticator;
import org.mkh.auth.LocalCookieAuthenticator;
import org.mkh.entity.User;
import org.mkh.entity.UserContext;
import org.mkh.ex.AuthenticateException;
@WebFilter(filterName = "SigninFilter", urlPatterns = {"/index", "/users"})
public class SigninFilter implements Filter {
private Authenticator[] authenticators = initAuthenticators();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// TODO Auto-generated method stub
}
private Authenticator[] initAuthenticators() {
Authenticator[] authenticators = new Authenticator[] {new LocalCookieAuthenticator()};
return authenticators;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("start filter................");
HttpServletResponse resp = (HttpServletResponse)response;
HttpServletRequest req = (HttpServletRequest)request;
User user = tryGetAuthenticatedUser(req, resp);
if (user == null) {
resp.sendRedirect("/signin");
return;
}
try (UserContext context = new UserContext(user)) {
chain.doFilter(request, response);
}
}
public User tryGetAuthenticatedUser(HttpServletRequest request, HttpServletResponse response) {
User user = null;
for (Authenticator authenticator : authenticators) {
try {
user = authenticator.authenticate(request, response);
} catch (AuthenticateException e) {
}
if (user != null) {
break;
}
}
return user;
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
}
6.修改启动类,加入注解扫描
@SpringBootApplication
@ServletComponentScan(basePackages = {"org.mkh.filter"})
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
7.修改登录的Controller,登录成功后把Cookie写到客户端
@PostMapping("/signin")
public String sign(String username, String password, HttpServletResponse resp) {
System.out.println(username + "-" + password);
User user = userService.getUserByUsername(username);
if (user != null && user.getPassword().equals(password)) {
// 生成cookie
Cookie cookie = CookieUtil.generateCookie(user);
System.out.println("write new cookie:" + cookie.getValue());
// 将cookie写入客户端
resp.addCookie(cookie);
return "redirect:/index";
}
return "signin-error";
}
8.启动项目,直接访问http://localhost:8080/users,由于没有登录,过滤器会拦截请求,重定向到登录页面,登录后可以正常访问,查看Cookie,如图,Cookie正常写入。
由于设置了cookie的过期时间为60秒,一分钟后再次访问,请求会再次被拦截。