Web登录功能及其安全验证
登录认证
业务分析
系统的登录认证,本质上是查询语句,查询数据库中用户名与密码是否与前端传来的相同。
列如前端来数据为
{
"username": "jinyong",
"password": "123456"
}
在controller层进行接收,并调用Service层的方法处理后响应结果
在Service层调用Mapper层方法查询username和password
在Mapper层进行sql语句查询
业务实现
controller
/**
* 登录
*
*/
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("登录成功");
LoginIfo loginIfo= empService.login(emp);
if (loginIfo!=null){
return Result.success(loginIfo);
}
return Result.error("用户名或者密码错误!");
}
Service
@Override
public LoginIfo login(Emp emp) {
Emp e = empMapper.getByUsernameAndPassword(emp);
//判断
if (e!=null){//登录成功,生成JWT令牌
Map<String,Object> claims=new HashMap<>();
claims.put("id",e.getId());
claims.put("username",e.getUsername());
claims.put("name",e.getName());
String jwt = JwtUtils.generateJwt(claims);
LoginIfo loginIfo = new LoginIfo(e.getId(),e.getUsername(),e.getName(),jwt);
return loginIfo;
}
return null;
}
Mapper
@Select("select * from emp where username=#{username} and password=#{password}")
Emp getByUsernameAndPassword(Emp emp);
登录校验
在完成登录功能之后,登录成功后就可以进入到后台管理系统中进行系统数据的操作,但是如果直接输入网址后,也能进行数据操作,我们需要对用户是否登录的状态进行判断,如果没有登录,则跳转登录页面进行登录。
所谓 登录校验,就是服务器端对浏览器的每次请求,都要进行一次检验用户是否登录,如果用户没有登录,不允许请求的业务操作,返回前端响应一个错误结果,并跳转到登录页面
问题分析
由于HTTP协议是无状态的,每次http协议请求之间都是独立的,当http在之前实现了登录操作,后面发送请求时,服务器无法判断是否登录
实现思路
- 在员工登录成功之后,需要将用户登录成功的信息存起来,记录用户已经登录成功的标记
- 在浏览器发起请求的时候,需要在服务器进行统一拦截,拦截之后进行登录校验
为了代码的简洁性,我们可以通过统一拦截的方式,列如过滤器或者拦截器等
因为在系统中的功能都需要进行登录校验,相同的代码逻辑,通过统一拦截技术,我们拦截浏览器发送的请求,拦截请求后,通过请求来获取之前发送的登录标记,如果可以获取到,则说明员工已经登录成功,我们之间放行,没有获取到登录标记,返回错误,注意在登录功能不需要获取登录标记
会话技术
我们在Web开发中,我们将浏览器和服务器之间的一次连接,我们就称为一次会话
在浏览器发起请求的时候,这个会话就建立了,一直到有任何一方断开连接,会话结束,一次会话可以有多次请求和响应。
会话跟踪
-
会话跟踪:
一种维护浏览器状态的方法,服务器需要识别多次请求是否是来自同一个浏览器,以便在同一个会话中多次请求共享数据
会话跟踪技术常见有三种:
- Cookie(客户端会话跟踪技术)
- 数据存储在客户端浏览器中
- Session(服务端会话跟踪技术)
- 数据存储在服务端
- 令牌技术
方案一:Cookie
方案介绍
cookie是客户端会话跟踪技术,它是存储在客户端浏览器的,我们在浏览器第一次发起请求时,我们在服务器端设置一个cookie,我们在cookie中我们可以来存储用户的一些数据信息,在第一次服务器端在给客户端响应数据的时候(登录),会自动的将cookie响应给浏览器本地,接下来的每次浏览器请求,都会将浏览器本地储存的cookie携带到请求中,服务器来判断请求中是否存在cookie是否存在,如果不存在这个cookie,就说明客户端没有进行登录操作,禁止进行操作,如果存在,则说明已经进行登录完成。
由于cookie是HTTP协议中支持的技术,所以服务器和浏览器可以自动携带,并且提供了一个响应头和请求头用来携带cookie
- 响应头 Set-Cookie:设置Cookie数据
- 请求头 Cookie:携带Cookie数据
代码测试
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}
优点
HTTP协议中支持的技术,浏览器和服务器携带的cookie都是自动进行,无需手动操作
缺点
- 移动端App无法使用
- 不安全,浏览器可以禁用Cookie
- Cookie不能跨域
方案二:Session
方案介绍
服务器端会话跟踪技术,因为他是存储在服务器端的
浏览器在第一次请求服务器的时候,我们可以在服务器中来获取会话对象Session,如果是第一次请求Session,由于会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session。接下来服务器端再给浏览器每次响应数据的时候,它会将Session的ID通过Cookie响应给浏览器,其实在响应头当中增加了一个Set-Cookie,cookie的名字是固定的服务器端会话对象id,浏览器会自动识别这个响应头,然后自动的将cookie存储在浏览器本地
后续的每次请求当中,都会将Cookie的数据获取出来,并且携带到服务端,然后根据cookie的数据跟众多的Sesion中来找到当前请求对应的会话对象Session
代码
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_username","itheima")); //设置Cookie/响应Cookie
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")){
System.out.println("login_username: "+cookie.getValue()); //输出name为login_username的cookie
}
}
return Result.success();
}
}
优点
Session是存储在服务端的,安全
缺点
- 服务器集群环境下无法直接使用Session
- 移动端APP(Android、IOS)中无法使用Cookie
- 用户可以自己禁用Cookie
- Cookie不能跨域
方案三:令牌技术
方案介绍
其实它就是一个用户身份的标识,本质上是一个字符串。
在登陆成功后,我们生成一个令牌,令牌作为用户的合法凭证,接下来在响应数据的时候,我们就可以直接将令牌响应给前端。
后续的每一次请求,都会将令牌携带给服务端,携带到服务器端后,检验令牌是否有效,如果有效则说明用户已经完成登录操作,否则反之
如果我们在同一次会话的多次请求,想要共享数据,我们就可以将共享的数据存储在令牌当中
优点
- 支持PC端、移动端
- 解决集群环境下的认证问题
缺点
要自己实现(包括令牌的生成、令牌的传递、令牌的校验)
JWT令牌
基于令牌技术来实现会话追踪,令牌也就是用户登录的身份标识,其本质是一个字符串,令牌技术主要使用jwt令牌
介绍
jwt用json的数据格式传输信息,也就是说jwt是一个简单的字符串,可以在请求参数和请求头中传递,并且jwt令牌可以根据需求自定义数据内容。
组成
简单来所,jwt令牌分为三个部分
- header(头)
记录令牌类型,算法等,列如
{"alg":"HS256","type":"JWT"}
- payload(有效载荷)
携带自定义的信息,列如
:{"id":"1","username":"Tom"}
- Signature(签名)
签名的作用是为了防止jwt令牌被篡改,保证jwt令牌的安全性可靠性,如果jwt令牌中被改动,jwt令牌校验就会失败
JWT令牌JSON转换Base64
jwt令牌在生成的时候,会对json格式的数据进行编码
也就是base64编码
Base64是一种编码形式,而不是加密
应用场景
我们通常将生成jwt令牌的放到登录操作阶段
- 浏览器发起登录请求,如果登录成功,则需要生成jwt令牌,将jwt令牌返回给前端
- 前端会将令牌储存起来,作为登录成功的标识,每一次请求都会将jwt令牌携带给服务端
- 服务端每次拦截请求时,都会检验令牌是否存在,是否有效,如果有效则,直接放行请求的处理
生成和校验
JWT的生成需要引入他的依赖
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
引入jwt依赖后,调用工具包中提供的API来完成JWT令牌的生成和校验
工具包类:Jwts
实现代码:
@Test
public void testGenJwt() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 10);
claims.put("username", "itheima");
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "aXRjYXN0")
.addClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 12 * 3600 * 1000))
.compact();
System.out.println(jwt);
}
其输出的结果为JWT令牌
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk
校验令牌
@Test
public void testParseJwt() {
Claims claims = Jwts.parser().setSigningKey("aXRjYXN0")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTAsInVzZXJuYW1lIjoiaXRoZWltYSIsImV4cCI6MTcwMTkwOTAxNX0.N-MD6DmoeIIY5lB5z73UFLN9u7veppx1K5_N_jS9Yko")
.getBody();
System.out.println(claims);
}
运行测试的方式
{id=10, username=itheima, exp=1701909015}

浙公网安备 33010602011771号