Preface
As a JAVA developer, I went to interview several times before. The interviewer asked me how well I mastered JAVAWeb. I didn't know how to answer at that time. What was used in the daily development of the Web? Today we talk about the three contents that JAVAWeb should master most.
development path
1. A long, long time ago, the Web was basically the browsing of documents. Since it is browsing, as a server, there is no need to record who has browsed what documents in a certain period of time. Each request is a new HTTP protocol, which is a request. Add response, especially since I don’t have to remember who made the HTTP request just now, every request is brand new to me.
2. However, with the rise of interactive web applications, such as online shopping sites, sites that need to be logged in, etc., we immediately face a problem, that is, to manage the session, we must remember who is logged in to the system and who is shopping at your own Putting goods in the car means that I have to distinguish everyone. This is no small challenge. Because HTTP requests are stateless, the way I figured out is to send everyone a session id. To put it bluntly It’s just a random string, and everyone receives it differently. Every time you make an HTTP request to me, you will pass this string together so that I can distinguish who is who.
3. This way everyone is very happy, but the server is not happy, everyone only needs to save their own session id, and the server needs to save everyone's session id! If there are more access servers, there will be thousands or even hundreds of thousands.
This is a huge overhead for the server, which severely limits the server's scalability. For example, I used two machines to form a cluster, and Xiao F logged in to the system through machine A, and the session id would be stored on machine A. Suppose What if the next request of small F is forwarded to machine B? Machine B does not have the session id of Little F.
Sometimes a little trick is used: session sticky, which is to keep small F's request stuck on machine A, but it doesn't work. If machine A hangs up, it has to go to machine B.
So I have to copy the session, and move the session id between the two machines. I'm almost exhausted.
Later, there was a trick called Memcached: store the session id centrally in one place, and all machines will access the data in this place. In this way, there is no need to copy, but it increases the possibility of a single point of failure. The machine responsible for the session has hung up, and everyone has to log in again. It is estimated that they will be scolded to death.
I also tried to cluster this single-point machine to increase reliability, but no matter what, this small session is a heavy burden for me.
4 So some people have been thinking, why should I save this nasty session? How good is it to let each client save it?
But if I don't save these session ids, how can I verify that the session ids sent to me by the client are indeed generated by me? If we don't verify, we don't know if they are legally logged in users. Those malicious guys can fake the session id and do whatever they want.
Well, yes, the key point is verification!
For example, Xiao F has logged in to the system, and I send him a token, which contains the user id of Xiao F. The next time Xiao F accesses me through the Http request again, bring this token through the Http header. Just come here.
But there is no essential difference between this and session id. Anyone can forge it, so I have to think of a way to make it impossible for others to forge it.
Then make a signature on the data. For example, I use the HMAC-SHA256 algorithm and add a key that only I know to sign the data. Use this signature and the data together as a token. Because the key is unknown to others , The token cannot be forged.
I don’t save this token. When Xiao F sends me this token, I use the same HMAC-SHA256 algorithm and the same key to calculate the signature for the data again, and compare it with the signature in the token. If they are the same, I know that Little F has logged in and can directly get the user id of Little F. If they are not the same, the data part must have been tampered with. I will tell the sender: Sorry, no authentication.
The data in the token is stored in plain text (although I will use Base64 for encoding, but that is not encrypted), it can still be seen by others, so I cannot store sensitive information like passwords in it.
Of course, if a person's token is stolen by others, then I can't help it. I will also think that the thief is a legitimate user, which is actually the same as a person's session id being stolen by others.
In this way, I will not save the session id, I just generate the token, and then verify the token, I use my CPU calculation time to get my session storage space!
The burden of session id is relieved. It can be said that it is nothing to do. My machine cluster can now be easily expanded horizontally, and the number of user visits increases. Just add machines directly. This stateless feeling is really great!
Cookie
1. What is a cookie
Cookie translated into Chinese means "小甜饼", which was proposed by the W3C organization and was originally a mechanism developed by the Netscape community. At present, cookies have become a standard, and all major browsers such as IE, Netscape, Firefox, Opera, etc. support cookies.
The server has no way of knowing the identity of the client from the network connection. How to do it? Just issue a pass to the clients, one for each person, whoever visits must bring their own pass. In this way, the server can confirm the identity of the client from the pass. This is how cookies work.
Cookie is a mechanism for the client to save user information. It is used to record some user information and is also a way to implement Session. The amount of data stored in cookies is limited, and they are all stored in the client browser. Different browsers have different storage sizes, but generally do not exceed 4KB. Therefore, the use of cookies can actually only store a small piece of text information (key-value format).
2. Cookie mechanism
When a user visits and logs in to a website for the first time, the cookie setting and sending will go through the following 4 steps:
- The client sends a request to the server;
- The server sends an HttpResponse response to the client, which contains the header of Set-Cookie;
- The client saves the cookie, and then sends a request to the server, the HttpRequest request will contain a Cookie header;
- The server returns the response data.
In order to explore this process, I wrote the code for testing, as follows:
In the doGet method, I new a Cookie object and added it to the HttpResponse object
@RestController
public class TestController {
@GetMapping(value = "/doGet")
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 设置生命周期为MAX_VALUE
cookie.setMaxAge(Integer.MAX_VALUE);
resp.addCookie(cookie);
}
}
The browser enters the address for access, and the result is shown in the figure:
shows that the Response Headers contains the Set-Cookie header, and the Request Headers contains the Cookie header. The name and value are exactly the above settings.
3. Cookie properties
Expires
This attribute is used to set the validity period of the Cookie. The maxAge in Cookie is used to represent this attribute, and the unit is second. This attribute is read and written in Cookie through getMaxAge() and setMaxAge(int maxAge). There are 3 types of maxAge values, which are positive, negative and 0.
If the maxAge attribute is a positive number, it means that the cookie will automatically expire after maxAge seconds. The browser will persist the cookie whose maxAge is a positive number, that is, write it into the corresponding cookie file (the storage location of each browser is inconsistent). Regardless of whether the customer closes the browser or computer, the cookie is still valid when logging in to the website as long as it is maxAge seconds before. The cookie information in the code below will always be valid.
Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 设置生命周期为MAX_VALUE,永久有效
cookie.setMaxAge(Integer.MAX_VALUE);
resp.addCookie(cookie);
When the maxAge attribute is a negative number, it means that the cookie is only a temporary cookie and will not be persisted. It is only valid in this browser window or the child window opened in this window. The cookie will become invalid immediately after closing the browser.
Cookie cookie = new Cookie("jiangwang",System.currentTimeMillis()+"");
// 设置生命周期为MAX_VALUE,永久有效
cookie.setMaxAge(-1);
resp.addCookie(cookie);
When maxAge is 0, it means to delete cookies immediately.
Cookie[] cookies = req.getCookies();
Cookie cookie = null;
// get Cookie
for (Cookie ck : cookies) {
if ("jiangwang".equals(ck.getName())) {
cookie = ck;
break;
}
}
if (null != cookie) {
// 删除一个cookie
cookie.setMaxAge(0);
resp.addCookie(cookie);
}
Modify or delete cookies
The Cookie operation provided by HttpServletResponse has only one addCookie (Cookie cookie), so if you want to modify a Cookie, you can only use a Cookie with the same name to overwrite the original Cookie. If you want to delete a cookie, you only need to create a new cookie with the same name, set maxAge to 0, and overwrite the original cookie.
For the newly created Cookie, all attributes other than value and maxAge, such as name, path, and domain, must be consistent with the original ones in order to achieve the effect of modification or deletion. Otherwise, the browser will treat it as two different cookies and will not cover them.
Cookie's domain name
Cookies cannot cross domain names, and the privacy security mechanism prohibits websites from illegally obtaining cookies from other websites.
Under normal circumstances, two second-level domain names under the same first-level domain name cannot use cookies interchangeably, such as a1.jiangwang.com
and a2.jiangwang.com
, because the two domain names are not exactly the same. If you want jiangwnag.com
to use the cookie, you need to set the cookie domain parameter to .jiangwang.com
, so that you can access the same cookie a1.jiangwang.com
and a2.jiangwang.com
The first-level domain name is also called the top-level domain name, which is generally composed of a string + suffix. Familiar first-level domain names are baidu.com and qq.com. com, cn, net, etc. are common suffixes.
The second-level domain name is derived from the first-level domain name. Forabc.com
, ifblog.abc.com
andwww.abc.com
are both derived from the second-level domain name.
Cookie path
The path attribute determines the path allowed to access the cookie. For example, set to "/" to allow cookies to be used in all paths
4. Application
The most typical application of cookies is to determine whether a registered user has logged in to a website. The user may be prompted whether to retain user information when entering the website next time to simplify the login procedure. These are the functions of cookies. Another important application is "shopping cart" processing. Users may select different products on different pages of the same website within a period of time, and this information will be written into Cookies so that the information can be extracted at the time of the final payment.
Session
1. What is Session
In WEB development, the server can create a session object (session object) for each user's browser. Note: A browser exclusively owns a session object (by default). Therefore, when user data needs to be saved, the server program can write the user data into the user's browser exclusive session. When the user uses the browser to access other programs, other programs can retrieve the user's data from the user's session. User service.
2. Session implementation principle
After the server creates the session, it will write the session id number back to the client in the form of a cookie. In this way, as long as the client's browser is not turned off, the server will bring the session id number with it when accessing the server again. It is found that the client browser comes with the session id, and it will use the corresponding session in the memory to serve it. You can use the following code to prove:
@RestController
public class TestController {
@GetMapping(value = "/doGet")
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
//使用request对象的getSession()获取session,如果session不存在则创建一个
HttpSession session = request.getSession();
//将数据存储到session中
session.setAttribute("mayun", "马云");
//获取session的Id
String sessionId = session.getId();
//判断session是不是新创建的
if (session.isNew()) {
response.getWriter().print("session创建成功,session的id是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在该session了,session的id是:"+sessionId);
}
}
}
At the first visit, the server will create a new sesion and send the session ID to the client browser in the form of a cookie, as shown in the following figure:
Request the server again, and you can see that when the browser requests the server again, it will pass the session Id stored in the cookie to the server side, as shown in the following figure:
3. Session creation and destruction
A new Session will be created when the request.getSession() method is called for the first time in the program, and the isNew() method can be used to determine whether the Session is newly created
//使用request对象的getSession()获取session,如果session不存在则创建一个
HttpSession session = request.getSession();
//获取session的Id
String sessionId = session.getId();
//判断session是不是新创建的
if (session.isNew()) {
response.getWriter().print("session创建成功,session的id是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在session,session的id是:"+sessionId);
}
If the session object is not used for 30 minutes by default, the server will automatically destroy the session, or you can manually configure the expiration time of the session, for example:
session.setMaxInactiveInterval(10*60);//10分钟后session失效
When you need to manually set the Session invalidation in the program, you can manually call the session.invalidate method to destroy the session.
HttpSession session = request.getSession();
//手工调用session.invalidate方法,摧毁session
session.invalidate();
Interview question: When the browser is closed, the session is destroyed? wrong.
After the session is generated, as long as the user continues to access, the server will update the last access time of the session and maintain the session. To prevent memory overflow, the server deletes sessions that have not been active for a long time from memory. This time is the timeout period of the Session. If the server is not accessed after the timeout period expires, the Session will automatically become invalid.
Token
1. What is Token
Token means "token", which is a string of strings generated by the server as an identifier for the client to make a request.
When the user logs in for the first time, the server generates a token and returns this token to the client. In the future, the client only needs to bring this token to request data without having to bring the user name and password again.
The composition of a simple token; uid (user's unique identity), time (time stamp of the current time), sign (signature, the first few digits of the token are compressed into a hexadecimal string of a certain length by a hash algorithm.) Prevent token leakage).
2. Token principle
- User sends request with username and password
- Program verification
- The program returns a Token to the client
- The client stores the Token, and each request is sent with the Token
- The server verifies the Token and returns the data
3. Use of Token
Spring Boot and Jwt integration example
The project depends on pom.xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
Custom annotation
//需要登录才能进行操作的注解LoginToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
boolean required() default true;
}
//用来跳过验证的PassToken
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
User entity class, and query service
public class User {
private String userID;
private String userName;
private String passWord;
public String getUserID() {
return userID;
}
public void setUserID(String 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;
}
}
@Service
public class UserService {
public User getUser(String userid, String password){
if ("admin".equals(userid) && "admin".equals(password)){
User user=new User();
user.setUserID("admin");
user.setUserName("admin");
user.setPassWord("admin");
return user;
}
else{
return null;
}
}
public User getUser(String userid){
if ("admin".equals(userid)){
User user=new User();
user.setUserID("admin");
user.setUserName("admin");
user.setPassWord("admin");
return user;
}
else{
return null;
}
}
}
Token generation
@Service
public class TokenService {
/**
* 过期时间10分钟
*/
private static final long EXPIRE_TIME = 10 * 60 * 1000;
public String getToken(User user) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
String token="";
token= JWT.create().withAudience(user.getUserID()) // 将 user id 保存到 token 里面
.withExpiresAt(date) //十分钟后token过期
.sign(Algorithm.HMAC256(user.getPassWord())); // 以 password 作为 token 的密钥
return token;
}
}
Interceptor intercepts token
package com.jw.interceptor;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.jw.annotation.LoginToken;
import com.jw.annotation.PassToken;
import com.jw.entity.User;
import com.jw.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
public class JwtInterceptor implements HandlerInterceptor{
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
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;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken loginToken = method.getAnnotation(LoginToken.class);
if (loginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
User user = userService.getUser(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassWord())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
Register interceptor
package com.jw.config;
import com.jw.interceptor.JwtInterceptor;
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 InterceptorConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
//注册TestInterceptor拦截器
// InterceptorRegistration registration = registry.addInterceptor(jwtInterceptor());
// registration.addPathPatterns("/**"); //添加拦截路径
// registration.excludePathPatterns( //添加不拦截路径
// "/**/*.html", //html静态资源
// "/**/*.js", //js静态资源
// "/**/*.css", //css静态资源
// "/**/*.woff",
// "/**/*.ttf",
// "/swagger-ui.html"
// );
}
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
}
Log in to the Controller
@RestController
public class LoginController {
@Autowired
private UserService userService;
@Autowired
private TokenService tokenService;
@PostMapping("login")
public Object login(String username, String password){
JSONObject jsonObject=new JSONObject();
User user=userService.getUser(username, password);
if(user==null){
jsonObject.put("message","登录失败!");
return jsonObject;
}else {
String token = tokenService.getToken(user);
jsonObject.put("token", token);
jsonObject.put("user", user);
return jsonObject;
}
}
@LoginToken
@GetMapping("/getMessage")
public String getMessage(){
return "你已通过验证";
}
}
Configure global exception capture
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(Exception.class)
public Object handleException(Exception e) {
String msg = e.getMessage();
if (msg == null || msg.equals("")) {
msg = "服务器出错";
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 1000);
jsonObject.put("message", msg);
return jsonObject;
}
}
postman test
Get token
No token login
Login with token
Wrong token login
4. Token's advantages and disadvantages
advantages:
- Support cross-domain access: Cookie does not allow cross-domain access, token support;
- Stateless: token is stateless, session is stateful;
- Decoupling: No need to bind to a specific authentication scheme. Token can be generated anywhere, as long as you can make a Token generation call when your API is called;
- More suitable for mobile applications: Cookies do not support mobile phone access;
- Performance: In the process of network transmission, the performance is better;
- Based on standardization: Your API can use standardized JSON Web Token (JWT). This standard already has multiple back-end libraries (.NET, Ruby, Java, Python, PHP) and the support of many companies (such as: Firebase, Google , Microsoft).
disadvantages:
- Occupy bandwidth, under normal circumstances, it is larger than session_id, it needs to consume more traffic and squeeze more bandwidth. If your website has 100,000 browsers per month, it means that it will cost tens of megabytes of traffic more. It doesn't sound like much, but the accumulation of time is not a small expense. In fact, many people will store more information in JWT;
- Unable to log off on the server, it is difficult to solve the hijacking problem for so long;
- For performance issues, one of the selling points of JWT is cryptographic signatures. Because of this feature, the receiver can verify that the JWT is valid and trusted. But in most web authentication applications, JWT will be stored in a cookie, which means that you have two levels of signature. It sounds awesome, but there is no advantage. For this, you need to spend twice the CPU overhead to verify the signature. For Web applications with strict performance requirements, this is not ideal, especially for single-threaded environments.
end
I am a code farmer who is being beaten and working hard to advance. If the article is helpful to you, remember to like and follow, thank you!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。