尚庭公寓--------登陆流程介绍以及功能代码
登陆认证
基于Session的认证流程
优点
- 用户友好:
• 用户登录后,无需在每个页面请求中重复输入用户名和密码。• 提供了一种无缝的用户体验,用户在浏览网站时不会频繁被要求重新认证。 - 安全性:
• 服务器端维护用户状态信息,而客户端仅存储一个Session ID,这样可以减少敏感信息的暴露。
• Session ID通常具有时效性,可以设置过期时间,增加了安全性。 - 易于管理:
• 服务器可以轻松地管理用户会话,例如,可以控制Session的生命周期,包括创建、更新和销毁Session。
• 可以对Session进行序列化,以便在服务器重启时恢复用户会话。 - 灵活性:
• 可以存储用户特定的信息,如用户角色、权限等,以便在用户会话期间使用。
• 可以根据需要自定义Session的行为,例如,可以设置Session的有效期、锁定机制等。 - 支持分布式部署:
• 在分布式系统中,可以通过共享Session存储或使用Session复制技术来支持Session的一致性。 - 防止CSRF攻击:
• 通过在Session中存储CSRF令牌,并在表单提交时验证令牌,可以有效地防止跨站请求伪造(CSRF)攻击。
基于Token的认证流程
优点
- 无状态和可扩展性:
- • 基于Token的认证是无状态的,服务器不需要存储Session信息,这使得系统更容易扩展,特别是在分布式系统中。
- 安全性:
• Token通常经过数字签名,这确保了Token在传输过程中未被篡改。
• 可以使用强大的加密算法来生成Token,增加了安全性。 - 支持跨域认证:
• 由于Token是自包含的,它可以在多个域之间安全地传递,这使得它非常适合单点登录(SSO)场景。 - 自定义性强:
• Token可以包含丰富的用户信息和权限数据,这些信息可以根据需要进行定制。 - 减少服务器负担:
• 由于服务器不需要存储Session信息,这减少了服务器的存储和内存负担。 - 支持移动和分布式设备:
• Token可以在多种环境中使用,包括移动设备和分布式系统中,这使得它非常适合现代的移动和云应用。 - 简单易于实现:
• 基于Token的认证流程相对简单,易于实现和维护。
Token详解 :
Token是一种令牌,它在计算机身份验证中用于代表用户身份或会话。在Web开发中,Token通常用于用户认证和授权,尤其是在无状态的API服务和单点登录(SSO)系统中。以下是对Token的详细解释:
Token的组成
以JWT(JSON Web Tokens)为例,一个Token通常由三部分组成,用点(.)分隔:
- Header(头部):
• 描述Token的元数据,例如Token的类型(JWT)和使用的签名算法(如HMAC SHA256或RSA)。
- Payload(负载):
• 包含声明(Claims),即有关实体(通常是用户)和其他数据的声明。
• 可以包含用户的角色、权限、Token的发行者、过期时间等信息。
- Signature(签名):
• 用于验证Token在传输过程中未被篡改。
• 通过使用头部指定的算法和密钥对头部和负载进行签名生成。
Token的安全性
数字签名:
• 使用私钥对Token进行签名,确保Token的完整性和真实性。
加密:
• 使用对称或非对称加密算法对Token进行加密,确保只有授权的接收者才能解密和读取Token内容。
过期时间:
• 设置Token的过期时间,过期后Token将失效,需要重新认证获取新的Token。
HTTPS:
• 在传输过程中使用HTTPS,防止Token在传输过程中被截获。
尚庭公寓后台管理系统登陆流程
根据上述图片我们可以分析出总共需要完成三个接口,以下是接口定义以及接口代码
获取图形验证码
导入maven
依赖
<dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置application.yml
spring:data:redis:host: <hostname>port: <port>database: 0
controller
层
package com.nie.lease.web.admin.controller.login;import com.nie.lease.common.login.LoginUserHolder;
import com.nie.lease.common.result.Result;
import com.nie.lease.common.utils.JwtUtils;
import com.nie.lease.web.admin.service.LoginService;
import com.nie.lease.web.admin.vo.login.CaptchaVo;
import com.nie.lease.web.admin.vo.login.LoginVo;
import com.nie.lease.web.admin.vo.system.user.SystemUserInfoVo;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;@Tag(name = "后台管理系统登录管理")
@RestController
@RequestMapping("/admin")
public class LoginController {@Autowiredprivate LoginService service;@Operation(summary = "获取图形验证码")@GetMapping("login/captcha")public Result<CaptchaVo> getCaptcha() {CaptchaVo result=service.getCaptcha();return Result.ok(result);}
service
接口
package com.nie.lease.web.admin.service;import com.nie.lease.web.admin.vo.login.CaptchaVo;
import com.nie.lease.web.admin.vo.login.LoginVo;
import com.nie.lease.web.admin.vo.system.user.SystemUserInfoVo;public interface LoginService {CaptchaVo getCaptcha();}
service
实现类
@Overridepublic CaptchaVo getCaptcha() {SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);String code = specCaptcha.text().toLowerCase();String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID();stringRedisTemplate.opsForValue().set(key,code,RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC, TimeUnit.SECONDS);return new CaptchaVo(specCaptcha.toBase64(),key);}
测试结果如下:
登录
导入maven
依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId>
</dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><scope>runtime</scope>
</dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><scope>runtime</scope>
</dependency>
创建Jwt工具类
package com.nie.lease.common.utils;import com.nie.lease.common.exception.LeaseException;
import com.nie.lease.common.result.ResultCodeEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;import javax.crypto.SecretKey;
import java.util.Date;public class JwtUtils {private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());public static String createToken(Long userId, String username) {String token = Jwts.builder().setSubject("USER_INFO").setExpiration(new Date(System.currentTimeMillis() + 3600000)).claim("userId", userId).claim("username", username).signWith(tokenSignKey).compact();return token;}public static void main(String[] args) {System.out.println(createToken(2L, "user"));}public static Claims parseToken(String token){if (token==null){throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);}try {JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(tokenSignKey).build();Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);return claimsJws.getBody();}catch (ExpiredJwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);}catch (JwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);}}
}
controller
层
@Operation(summary = "登录")@PostMapping("login")public Result<String> login(@RequestBody LoginVo loginVo) {String token = service.login(loginVo);return Result.ok(token);}
service
接口
String login(LoginVo loginVo);
service
实现类
@Overridepublic String login(LoginVo loginVo) {//1.判断是否输入了验证码if (!StringUtils.hasText(loginVo.getCaptchaCode())) {throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND);}//2.校验验证码String code = stringRedisTemplate.opsForValue().get(loginVo.getCaptchaKey());if (code == null) {throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED);}if (!code.equals(loginVo.getCaptchaCode().toLowerCase())) {throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR);}//3.校验用户是否存在SystemUser systemUser = systemUserMapper.selectOneByUsername(loginVo.getUsername());if (systemUser == null) {throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR);}//4.校验用户是否被禁if (systemUser.getStatus() == BaseStatus.DISABLE) {throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);}//5.校验用户密码if (!systemUser.getPassword().equals(DigestUtils.md5Hex(loginVo.getPassword()))) {throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);}//6.创建并返回TOKENreturn JwtUtils.createToken(systemUser.getId(), systemUser.getUsername());}
mapper
接口
SystemUser selectOneByUsername(String username);
mapper
xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nie.lease.web.admin.mapper.SystemUserMapper"><select id="pageSystemUser" resultType="com.nie.lease.web.admin.vo.system.user.SystemUserItemVo">select su.id,username,su.name,type,phone,avatar_url,additional_info,post_id,su.status,sp.name post_namefrom system_user suleft join system_post sp on su.post_id = sp.id and sp.is_deleted = 0<where>su.is_deleted = 0<if test="queryVo.name != null and queryVo.name != ''">and su.name like concat('%',#{queryVo.name},'%')</if><if test="queryVo.phone !=null and queryVo.phone != ''">and su.phone like concat('%',#{queryVo.phone},'%')</if></where></select><select id="selectOneByUsername" resultType="com.nie.lease.model.entity.SystemUser">select id,username,password,name,type,phone,avatar_url,additional_info,post_id,statusfrom system_userwhere is_deleted = 0and username = #{username}</select>
</mapper>
编写拦截器
书写解析token的方法
package com.nie.lease.common.utils;import com.nie.lease.common.exception.LeaseException;
import com.nie.lease.common.result.ResultCodeEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;import javax.crypto.SecretKey;
import java.util.Date;public class JwtUtils {private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());public static String createToken(Long userId, String username) {String token = Jwts.builder().setSubject("USER_INFO").setExpiration(new Date(System.currentTimeMillis() + 3600000)).claim("userId", userId).claim("username", username).signWith(tokenSignKey).compact();return token;}public static void main(String[] args) {System.out.println(createToken(2L, "user"));}public static Claims parseToken(String token){if (token==null){throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);}try {JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(tokenSignKey).build();Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);return claimsJws.getBody();}catch (ExpiredJwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);}catch (JwtException e){throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);}}
}
##### 编写拦截器
package com.nie.lease.web.admin.custom.interceptor;import com.nie.lease.common.exception.LeaseException;
import com.nie.lease.common.login.LoginUser;
import com.nie.lease.common.login.LoginUserHolder;
import com.nie.lease.common.result.ResultCodeEnum;
import com.nie.lease.common.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;@Component
public class AuthenticationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("access-token");Claims claims = JwtUtils.parseToken(token);Long userId = claims.get("userId", Long.class);String userName = claims.get("userName", String.class);LoginUserHolder.setLoginUser(new LoginUser(userId, userName));return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {LoginUserHolder.clear();}
}
注册拦截器
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {@Autowiredprivate StringToBaseEnumConverterFactory stringToBaseEnumConverterFactory;@Autowiredprivate AuthenticationInterceptor authenticationInterceptor;@Overridepublic void addFormatters(FormatterRegistry registry) {registry.addConverterFactory(this.stringToBaseEnumConverterFactory);}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(this.authenticationInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/login/**");}}