最近学习了一下springsecurity机制,完善一下若依的后端登陆处理部分。
先放一个框架图:
从前端的views/login开始:
使用了一个loginform对象来存储登录模块中的参数,界面正常通过<el-form><el-input>来实现输入框。
const loginForm = ref({
username: "",
password: "",
rememberMe: false,
code: "",
uuid: ""
});
验证码实现:
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
验证码是通过captchaEnabled这个布尔值来判断是否显示的,获取验证码的方法如下:
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img;
loginForm.value.uuid = res.uuid;
}
});
}
在里面主要通过getCodeImg()这个方法来获取验证码是否使用的布尔值,打开对应api中的方法,
// 获取验证码
export function getCodeImg() {
return request({
url: '/captchaImage',
headers: {
isToken: false
},
method: 'get',
timeout: 20000
})
}
@GetMapping("/captchaImage")
public AjaxResult getCode(HttpServletResponse response) throws IOException
{
AjaxResult ajax = AjaxResult.success();
boolean captchaEnabled = configService.selectCaptchaEnabled();
ajax.put("captchaEnabled", captchaEnabled);
if (!captchaEnabled)
{
return ajax;
}
// 保存验证码信息
String uuid = IdUtils.simpleUUID();
String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
String capStr = null, code = null;
BufferedImage image = null;
// 生成验证码
String captchaType = RuoYiConfig.getCaptchaType();
if ("math".equals(captchaType))
{
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);
}
catch (IOException e)
{
return AjaxResult.error(e.getMessage());
}
ajax.put("uuid", uuid);
ajax.put("img", Base64.encode(os.toByteArray()));
return ajax;
}
回到前端部分,将返回的captchaEnabled参数拿到之后就做判断,如果有就赋值并且显示验证码图片。
登录实现:
在登录按钮上绑定了登录事件,贴代码:
可以看到这边的步骤是通过调用proxy(使用getCurrentInstance获取的当前实例),找到当前表单调用validate方法进行表单校验,校验之后看是否保存账号密码,如果保存就将账号密码存进cookies(每次打开登陆界面的时候会线用cookies.get()获取一遍),然后调用store中保存的login方法,这里更改了代码,让登录之后默认跳转到根路径。
function handleLogin() {
proxy.$refs.loginRef.validate(valid => {
if (valid) {
loading.value = true;
// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 });
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 });
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 });
} else {
// 否则移除
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
// 调用action的登录方法
userStore.login(loginForm.value).then(() => {
const query = route.query;
console.log("query",query)
// const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
// if (cur !== "redirect") {
// acc[cur] = query[cur];
// }
// console.log("otherQueryParams:",otherQueryParams)
// return acc;
// }, {});
console.log("otherQueryParams")
router.push({path: "/"});
// path: redirect.value || "/", query: otherQueryParams
}).catch(() => {
loading.value = false;
// 重新获取验证码
if (captchaEnabled.value) {
getCode();
}
});
}
});
}
下面是store中的登录方法,向后端发送登录请求并拿到token进行保存
login(userInfo) {
const username = userInfo.username.trim()
const password = userInfo.password
const code = userInfo.code
const uuid = userInfo.uuid
return new Promise((resolve, reject) => {
login(username, password, code, uuid).then(res => {
setToken(res.token)
this.token = res.token
resolve()
}).catch(error => {
reject(error)
})
})
},
这里对应后端Controller层方法,主要是调用service层进行用户的校验,并向前端返回token信息:
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);
return ajax;
}
进入到service层:
public String login(String username, String password, String code, String uuid)
{
// 验证码校验
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
else
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
}
finally
{
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
在这里主要使用了 try块中的方法,先把用户名和密码存到Authentication中,然后设置到上下文中,这样springsecurity框架就能拿到账户数据,然后通过调用authenticationManager.authenticate()方法做账户校验,这里主要直接调用了UserDetailsServiceImpl.loadUserByUsername方法检查账户是否符合规范
UserDetailsServiceImpl实现了UserDetailsService接口,这样springsecurity就会每次去访问UserDetailsServiceImpl中的loadUserByUsername方法,我们也可以在这里去做从数据库中拿用户信息与权限的逻辑。
在校验过程中如果有错误就会抛出异常,若没有抛出异常则继续执行,serice中剩余的逻辑就是设置token令牌,存入redis中并返回token给controller层。此时登录接口执行完毕了。