必备技能java系列梳理的文章并不涉及造轮子,以若依框架为基础,分析微服务Spring Cloud的能力,并理清微服务在业务处理上搭建的应用层架构,不会追问技术实践的底层细节,目标是可以让有后端经验的非java相关的程序员可以使用Spring Cloud搭建属于自己的后端服务
认证模块目录结构
上面的结构来自于若依认证模块仓库
- src->main->java目录是SpringCloud项目在生成项目后的固定结构,
-
***.ruoyi.auth为项目自定义,包含了若依认证模块的业务实现,
1、controller 为项目配置文件
2、form为登录注册使用的数据对象
3、service为网关的业务层实现 - resources目录下为本地配置,
- target为编译后的文件目录。
为什么需要认证模块
上面是若依的架构图,从上图可以看出当客户端发起Http请求之后,会首先进入网关,在网关这一层需要做认证处理,这里的认证包括生成Token,用户登录业务、用户注册业务、记录登录信息等。
实际上就是负责用户身份验证和授权,之所以会抽离为单独模块,是为了单独维护这一块的逻辑,方便后续的扩展比如集成第三方认证(OAuth验证),这种业务分割的方式也可以复用到其他类似的模块中。
控制器(controller)
Controller 负责处理用户的请求并调用后端代码(通常是服务层或业务逻辑层)来处理业务需求。看一下若依是如何在控制器中分发路由的。
@RestController
public class TokenController
{
@Autowired
private TokenService tokenService;
@Autowired
private SysLoginService sysLoginService;
// R<?> 标明需要返回的数据格式
@PostMapping("login")
public R<?> login(@RequestBody LoginBody form)
{
// 用户登录
LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
// 获取登录token
return R.ok(tokenService.createToken(userInfo));
}
@DeleteMapping("logout")
public R<?> logout(HttpServletRequest request)
{
String token = SecurityUtils.getToken(request);
if (StringUtils.isNotEmpty(token))
{
String username = JwtUtils.getUserName(token);
// 删除用户缓存记录
AuthUtil.logoutByToken(token);
// 记录用户退出日志
sysLoginService.logout(username);
}
return R.ok();
}
@PostMapping("refresh")
public R<?> refresh(HttpServletRequest request)
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser))
{
// 刷新令牌有效期
tokenService.refreshToken(loginUser);
return R.ok();
}
return R.ok();
}
@PostMapping("register")
public R<?> register(@RequestBody RegisterBody registerBody)
{
// 用户注册
sysLoginService.register(registerBody.getUsername(), registerBody.getPassword());
return R.ok();
}
}
@RestController 注解表示生成resfull风格的路由,同时也表明该类是一个 Spring 的 Controller,可以处理 HTTP 请求。
@Autowired 注解表示自动注入依赖,也就是以类型的方式识别Bean,在当前的控制器中可以调用依赖中的方法。
@PostMapping(“login”) 用来处理Post类型的请求,匹配到login请求后进入对应的方法中。
***@DeleteMapping(“logout”)***用于处理 DELETE 类型的请求,匹配到logout请求后进入对应的方法中。
业务实现(Service)
登录业务
控制器中的login方法调用业务层(service)的具体实现,包括用户名密码校验、IP地址校验、用户信息校验、登录日志记录等:
流程图如下:
业务层实现:
public LoginUser login(String username, String password)
{
// 用户名或密码为空 错误
if (StringUtils.isAnyBlank(username, password))
{
recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写");
throw new ServiceException("用户/密码必须填写");
}
// 密码如果不在指定范围内 错误
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
{
recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围");
throw new ServiceException("用户密码不在指定范围");
}
// 用户名不在指定范围内 错误
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
{
recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
throw new ServiceException("用户名不在指定范围");
}
// IP黑名单校验
String blackStr = Convert.toStr(redisService.getCacheObject(CacheConstants.SYS_LOGIN_BLACKIPLIST));
if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr()))
{
recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "很遗憾,访问IP已被列入系统黑名单");
throw new ServiceException("很遗憾,访问IP已被列入系统黑名单");
}
// 查询用户信息
R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData()))
{
recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
throw new ServiceException("登录用户:" + username + " 不存在");
}
if (R.FAIL == userResult.getCode())
{
throw new ServiceException(userResult.getMsg());
}
LoginUser userInfo = userResult.getData();
SysUser user = userResult.getData().getSysUser();
if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");
throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
}
if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
throw new ServiceException("对不起,您的账号:" + username + " 已停用");
}
passwordService.validate(user, password);
recordLogService.recordLogininfor(username, Constants.LOGIN_SU***ESS, "登录成功");
return userInfo;
}
注册业务
控制器中的register方法调用业务层(service)的具体实现,包括用户名密码校验、用户是否已注册等:
流程图如下:
业务层实现:
public void register(String username, String password)
{
// 用户名或密码为空 错误
if (StringUtils.isAnyBlank(username, password))
{
throw new ServiceException("用户/密码必须填写");
}
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
{
throw new ServiceException("账户长度必须在2到20个字符之间");
}
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
{
throw new ServiceException("密码长度必须在5到20个字符之间");
}
// 注册用户信息
SysUser sysUser = new SysUser();
sysUser.setUserName(username);
sysUser.setNickName(username);
sysUser.setPassword(SecurityUtils.encryptPassword(password));
R<?> registerResult = remoteUserService.registerUserInfo(sysUser, SecurityConstants.INNER);
if (R.FAIL == registerResult.getCode())
{
throw new ServiceException(registerResult.getMsg());
}
recordLogService.recordLogininfor(username, Constants.REGISTER, "注册成功");
}
上述就是若依的登录注册流程,这中间还涉及到一些通用的工具类,可自行参考若依工具类代码。