小明网站双登录系统实现——微信授权登录+用户名密码登录完整指南
一、数据库设计
-- 用户表(支持双登录方式)
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名(唯一)',
`password` varchar(100) DEFAULT '' COMMENT '密码(本地登录用,第三方登录为空)',
`real_name` varchar(50) DEFAULT '' COMMENT '真实姓名',
`avatar` varchar(255) DEFAULT '' COMMENT '头像URL',
`email` varchar(100) DEFAULT '' COMMENT '邮箱',
`phone` varchar(20) DEFAULT '' COMMENT '手机号',
`provider` varchar(20) NOT NULL COMMENT '登录方式(wechat/local)',
`provider_id` varchar(100) DEFAULT NULL COMMENT '第三方用户唯一标识(微信openid)',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0禁用,1启用)',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
`refresh_token` varchar(255) DEFAULT NULL COMMENT '微信刷新令牌(AES加密存储)',
`refresh_token_expire_time` datetime DEFAULT NULL COMMENT '刷新令牌过期时间',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_provider_openid` (`provider`,`provider_id`) COMMENT '第三方账号唯一索引',
UNIQUE KEY `uk_phone` (`phone`),
UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
二、POM依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>dual-login-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<hutool.version>5.8.20</hutool.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
三、配置文件(application.yml)
server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
url: jdbc:mysql://localhost:3306/dual_login_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
wechat:
oauth:
client-id: ${WECHAT_APP_ID:wx_your_app_id}
client-secret: ${WECHAT_APP_SECRET:your_app_secret}
redirect-uri: ${WECHAT_REDIRECT_URI:http://localhost:8080/api/auth/wechat/callback}
auth-uri: https://open.weixin.qq.com/connect/qrconnect
token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
user-info-uri: https://api.weixin.qq.com/sns/userinfo
scope: snsapi_login
token-expiration: 7200
jwt:
secret: ${JWT_SECRET:your_strong_secret_key_32_chars_min}
expiration: 86400000
issuer: dual-login-system
crypto:
aes:
key: ${AES_SECRET_KEY:your_aes_secret_key_16_bytes}
logging:
level:
root: INFO
com.example: DEBUG
file:
name: logs/dual-login.log
四、核心实体类
4.1 用户实体(SysUser.java)
@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String realName;
private String avatar;
private String email;
private String phone;
@TableField("provider")
private String provider;
@TableField("provider_id")
private String providerId;
private Integer status;
private LocalDateTime lastLoginTime;
@TableField("refresh_token")
private String refreshToken;
@TableField("refresh_token_expire_time")
private LocalDateTime refreshTokenExpireTime;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
}
4.2 社交用户信息(SocialUserInfo.java)
@Data
public class SocialUserInfo {
private String openid;
private String nickname;
private String avatar;
private String gender;
private String provider;
}
4.3 登录请求封装类
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
4.4 注册请求封装类
@Data
public class RegisterRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Email(message = "邮箱格式不正确")
private String email;
}
五、工具类
5.1 JWT工具类(JwtUtils.java)
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
@Value("${jwt.issuer}")
private String issuer;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuer(issuer)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
5.2 加密工具类(CryptoUtils.java)
@Component
public class CryptoUtils {
private final AES aes;
public CryptoUtils(@Value("${crypto.aes.key}") String aesKey) {
if (aesKey.length() < 16) {
aesKey = String.format("%-16s", aesKey).substring(0, 16);
} else if (aesKey.length() > 16) {
aesKey = aesKey.substring(0, 16);
}
this.aes = SecureUtil.aes(aesKey.getBytes());
}
public String encrypt(String data) {
return aes.encryptHex(data);
}
public String decrypt(String encryptedData) {
return aes.decryptStr(encryptedData);
}
}
六、服务层
6.1 用户服务(UserService.java)
@Service
@RequiredArgsConstructor
public class UserService extends ServiceImpl<SysUserMapper, SysUser> {
private final PasswordEncoder passwordEncoder;
private final CryptoUtils cryptoUtils;
private final WechatAuthService wechatAuthService;
public SysUser loginWithPassword(String username, String password) {
SysUser user = findByUsername(username);
if (user == null) throw new RuntimeException("用户不存在");
if (!passwordEncoder.matches(password, user.getPassword())) throw new RuntimeException("密码错误");
if (user.getStatus() != 1) throw new RuntimeException("账户已被禁用");
user.setLastLoginTime(LocalDateTime.now());
updateById(user);
return user;
}
@Transactional
public SysUser register(RegisterRequest request) {
if (findByUsername(request.getUsername()) != null) throw new RuntimeException("用户名已存在");
if (findByPhone(request.getPhone()) != null) throw new RuntimeException("手机号已注册");
if (StrUtil.isNotBlank(request.getEmail()) && findByEmail(request.getEmail()) != null) throw new RuntimeException("邮箱已注册");
SysUser user = new SysUser();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setPhone(request.getPhone());
user.setEmail(request.getEmail());
user.setRealName(request.getUsername());
user.setProvider("local");
user.setProviderId(null);
user.setStatus(1);
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
save(user);
return user;
}
@Transactional
public SysUser findOrCreateByWechatInfo(SocialUserInfo socialInfo, WechatAuthService.TokenDTO tokenDTO) {
SysUser user = lambdaQuery()
.eq(SysUser::getProvider, "wechat")
.eq(SysUser::getProviderId, socialInfo.getOpenid())
.one();
if (user != null) {
user.setLastLoginTime(LocalDateTime.now());
user.setRefreshToken(cryptoUtils.encrypt(tokenDTO.getRefreshToken()));
user.setRefreshTokenExpireTime(calculateExpireTime(tokenDTO.getExpiresIn()));
updateById(user);
return user;
}
user = new SysUser();
user.setUsername(generateUniqueUsername(socialInfo.getNickname()));
user.setPassword("");
user.setRealName(socialInfo.getNickname());
user.setAvatar(socialInfo.getAvatar());
user.setProvider("wechat");
user.setProviderId(socialInfo.getOpenid());
user.setStatus(1);
user.setRefreshToken(cryptoUtils.encrypt(tokenDTO.getRefreshToken()));
user.setRefreshTokenExpireTime(calculateExpireTime(tokenDTO.getExpiresIn()));
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
save(user);
return user;
}
// 辅助方法省略...
}
6.2 微信授权服务(WechatAuthService.java)
@Service
@RequiredArgsConstructor
public class WechatAuthService {
private final WechatOAuthProperties wechatProps;
public String buildAuthUrl(String state) {
return UriComponentsBuilder.fromHttpUrl(wechatProps.getAuthUri())
.queryParam("appid", wechatProps.getClientId())
.queryParam("redirect_uri", wechatProps.getRedirectUri())
.queryParam("response_type", "code")
.queryParam("scope", wechatProps.getScope())
.queryParam("state", state)
.fragment("wechat_redirect")
.build().toUriString();
}
public TokenDTO getAccessToken(String code) {
String url = UriComponentsBuilder.fromHttpUrl(wechatProps.getTokenUri())
.queryParam("appid", wechatProps.getClientId())
.queryParam("secret", wechatProps.getClientSecret())
.queryParam("code", code)
.queryParam("grant_type", "authorization_code")
.build().toUriString();
String response = HttpUtil.get(url);
JSONObject json = JSONUtil.parseObj(response);
if (json.containsKey("errcode")) throw new RuntimeException("微信授权失败:" + json.getStr("errmsg"));
TokenDTO tokenDTO = new TokenDTO();
tokenDTO.setAccessToken(json.getStr("access_token"));
tokenDTO.setRefreshToken(json.getStr("refresh_token"));
tokenDTO.setOpenid(json.getStr("openid"));
tokenDTO.setExpiresIn(json.getInt("expires_in", 7200));
tokenDTO.setScope(json.getStr("scope"));
return tokenDTO;
}
// 其他方法省略...
}
七、控制器(AuthController.java)
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
private final WechatAuthService wechatAuthService;
private final JwtUtils jwtUtils;
private final CryptoUtils cryptoUtils;
@PostMapping("/login")
public Result<LoginResult> login(@RequestBody LoginRequest request) {
SysUser user = userService.loginWithPassword(request.getUsername(), request.getPassword());
UserDetails userDetails = new User(user.getUsername(), user.getPassword(), Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
String token = jwtUtils.generateToken(userDetails);
LoginResult result = new LoginResult();
result.setToken(token);
result.setUserId(user.getId());
result.setUsername(user.getUsername());
result.setAvatar(user.getAvatar());
result.setProvider(user.getProvider());
return Result.success(result);
}
@GetMapping("/wechat/login")
public void wechatLogin(HttpServletResponse response, HttpSession session) throws IOException {
String state = UUID.randomUUID().toString();
session.setAttribute("wechat_oauth_state", state);
String authUrl = wechatAuthService.buildAuthUrl(state);
response.sendRedirect(authUrl);
}
@GetMapping("/wechat/callback")
public ModelAndView wechatCallback(@RequestParam String code, @RequestParam String state, HttpSession session) {
String savedState = (String) session.getAttribute("wechat_oauth_state");
if (savedState == null || !savedState.equals(state)) {
return new ModelAndView("redirect:/login?error=invalid_state");
}
try {
WechatAuthService.TokenDTO tokenDTO = wechatAuthService.getAccessToken(code);
SocialUserInfo userInfo = wechatAuthService.getUserInfo(tokenDTO.getAccessToken(), tokenDTO.getOpenid());
SysUser user = userService.findOrCreateByWechatInfo(userInfo, tokenDTO);
UserDetails userDetails = new User(user.getUsername(), "", Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));
String token = jwtUtils.generateToken(userDetails);
return new ModelAndView("redirect:https://yourfrontend.com/login/success?token=" + token);
} catch (Exception e) {
return new ModelAndView("redirect:/login?error=" + e.getMessage());
} finally {
session.removeAttribute("wechat_oauth_state");
}
}
}
八、Spring Security配置(SecurityConfig.java)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
九、JWT认证过滤器(JwtAuthenticationFilter.java)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtils jwtUtils, UserDetailsService userDetailsService) {
this.jwtUtils = jwtUtils;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null) {
String username = jwtUtils.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtils.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
} catch (Exception e) {
logger.error("无法设置用户认证: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (headerAuth != null && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
十、前端实现(简化版)
10.1 登录页面(login.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>双登录系统</title>
<style>
/* 样式保持不变 */
</style>
</head>
<body>
<h2>欢迎登录</h2>
<div id="password-login-form">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" placeholder="请输入用户名">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" placeholder="请输入密码">
</div>
<button onclick="loginWithPassword()">登录</button>
</div>
<div class="divider"><span>或</span></div>
<div class="social-login">
<button class="social-btn wechat-btn" onclick="loginWithWechat()">微信登录</button>
</div>
<div style="margin-top: 20px; text-align: center;">
<span>还没有账号?</span>
<a href="#" onclick="showRegisterForm()">立即注册</a>
</div>
<div id="register-form" style="display: none; margin-top: 20px;">
<!-- 注册表单内容 -->
</div>
<script>
// JavaScript函数保持不变
</script>
</body>
</html>
十一、生产环境部署建议
-
安全增强
- 使用HTTPS加密传输
- 配置防火墙规则
- 定期更换JWT密钥和AES密钥
- 实现接口访问频率限制
-
监控与日志
- 集成ELK收集日志
- 配置Prometheus+Grafana监控
- 记录关键操作日志(登录、注册、权限变更)
-
高可用架构
- 使用Nginx做负载均衡
- 数据库主从复制
- Redis缓存热点数据
- 微服务化拆分(用户服务、认证服务)
-
用户体验优化
- 实现记住登录状态功能
- 添加图形验证码防刷
- 支持账号绑定/解绑功能
- 提供忘记密码重置功能
十二、总结
通过本文实现的双登录系统,小明网站同时支持了微信授权登录和用户名密码登录两种方式,并且:
- 共享用户体系:两种登录方式使用同一套用户表,通过
provider字段区分 - 完整的注册流程:支持用户名、密码、手机号、邮箱注册
- 安全的认证机制:使用JWT进行认证,Spring Security保护接口
- 生产级实现:包含错误处理、日志记录、加密存储等生产环境必要功能
这个系统可以直接应用于生产环境,也可以作为基础框架扩展其他登录方式(如QQ登录、微博登录等)。
❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!
本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/19271545

浙公网安备 33010602011771号