**
一. 登录
一.结合redis的验证码
页面打开就加载一个验证码信息,其中包括一个uuid 和一个验证码,将其uuid加上前缀然后存入到redis中,将uuid的大概信息当作key,将验证码结果当中value。最后设置其过期时间,一般也就是一两分钟,使用不需要担心验证码点击过多。难度很低,但是使用了redis,可以学习其思维
二.登录流程
这边比较特殊,我原本项目是结合的shiro+oauth2,这边使用的是spring-security ,我感觉两个框架的流程差不多,首先spring-security 将传入的信息封装到UsernamePasswordAuthenticationToken这个类里面,其核心验证方法为其内部类DaoAuthenticationProvider中的retrieveUser方法的 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 这一段,若依这边实现了其UserDetailsService 重写了其loadUserByUsername 根据其用户名信息查询数据库查出一个UserDetail的实体类,最后通过DaoAuthenticationProvider的additionalAuthenticationChecks方法来比对两个类里面的密码是否一致。一旦登录成功就获取一个uuid将其加密返回为token(jwt非对称加密)(),若依这里还将一些用户登录信息保存到了redis中,key值为token加上一段拼接string,默认有效期为30分钟。若依这里还放入redis的信息中包含了很多系统有关的内容,后续开发登录信息管理时我觉得应该是那里用到了
补充一个登录验证方法,就是单独实现了一下验证逻辑,将其注入spring容器中,登录会走这个方法
/**
* 登录校验方法
*
* @author ruoyi
*/
@Component
public class SysLoginService
{
@Autowired
private TokenService tokenService;
@Resource
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public String login(String username, String password, String code, String uuid)
{
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
if (captcha == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();
}
// 用户验证
Authentication authentication = null;
try
{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
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 CustomException(e.getMessage());
}
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}
}
三. 前端登录流程
本人前端做的不多了解不深,使用简单描述一下,首先就是发生请求,在这个方法内还做了有没有记住密码的操作,在
this.$store.dispatch(“Login”, this.loginForm)这一段中发送了请求,ctrl加点击“Login”进入下一步
这一步是发送请求做的操作
一旦登录成功就会被一个钩子函数所拦截,是在src下面的permission.js的,这段代码就是登录成功后一些操作,其中也获取了用户信息
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(res => {
// 拉取user_info
const roles = res.roles
store.dispatch('GenerateRoutes', { roles }).then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})
}).catch(err => {
store.dispatch('LogOut').then(() => {
Message.error(err)
next({ path: '/' })
})
})
} else {
next()
}
}
} else {
// 没有token
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
NProgress.done()
}
}
})
最终会调用这个方法,用户权限和用户的一些基本信息就是在这里获取的
// 获取用户信息
GetInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(res => {
const user = res.user
const avatar = user.avatar == "" ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions)
} else {
commit('SET_ROLES', ['ROLE_DEFAULT'])
}
commit('SET_NAME', user.userName)
commit('SET_AVATAR', avatar)
resolve(res)
}).catch(error => {
reject(error)
})
})
},
四. 获取用户信息的后端代码分析
记得之前提过我们将一些用户信息存到的redis中,这里就是这么实现的,通过获取请求中的token解析得到其中的uuid信息,拼接上请求头然后到redis中获取。
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getInfo")
public AjaxResult getInfo()
{
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
SysUser user = loginUser.getUser();
// 角色集合
Set<String> roles = permissionService.getRolePermission(user);
// 权限集合
Set<String> permissions = permissionService.getMenuPermission(user);
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
return ajax;
}
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
return null;
}
/**
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
同时如果只是想获取用户简单信息还可以使用SecurityUtils类中的一些方法,核心就是Authentication ,这个里面存着一些用户信息,
/**
* 获取用户
**/
public static LoginUser getLoginUser()
{
try
{
return (LoginUser) getAuthentication().getPrincipal();
}
catch (Exception e)
{
throw new CustomException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
}
}
/**
* 获取Authentication
*/
public static Authentication getAuthentication()
{
return SecurityContextHolder.getContext().getAuthentication();
}
这个里面的数据是若依自己存储的,存储方式其实也是通过token,使用的是过滤器
五 Token拦截 过期处理和刷新
这边的token是携带过来的,将取出的值作为redis的key值取出登录时存入的用户信息,然后对其数据进行简单的检验,如果通过了就会将其信息交给Spring Security 处理,具体如何解读下面那段代码可以参考
如果未通过就会被Spring Security当作异常处理,这边就比较复杂了,最终是 会去处理,这边若依是通过自定义AuthenticationEntryPointImpl 来处理的,具体逻辑比较多,可以去debug跑一下
/**
* token过滤器 验证token有效性
*
* @author ruoyi
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
/**
* 认证失败处理类 返回未授权
*
* @author ruoyi
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException
{
int code = HttpStatus.HTTP_UNAUTHORIZED;
String msg = StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
六 退出登录
若依在SecurityConfig类中设置各个访问路由的时候设置了退出访问路径,将会访问自定义的退出类LogoutSuccessHandlerImpl 当中,综合上述我们可以得知,若依的登录就是在登录时将用户信息存入redis当中,key值写入token,当后续请求访问进来的时候会提取token当中的key去取用户信息,来实现权限的认证,所以当退出的时候就必须一是去除请求当中的token,二是删除有redis当中信息,这就是自定义退出类的实现逻辑
package com.wzld.framework.security.handle;
import cn.hutool.core.lang.Validator;
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import com.wzld.common.constant.Constants;
import com.wzld.common.core.domain.AjaxResult;
import com.wzld.common.core.domain.model.LoginUser;
import com.wzld.common.utils.ServletUtils;
import com.wzld.framework.manager.AsyncManager;
import com.wzld.framework.manager.factory.AsyncFactory;
import com.wzld.framework.web.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义退出处理类 返回成功
*
* @author ruoyi
*/
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
@Autowired
private TokenService tokenService;
/**
* 退出处理
*
* @return
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (Validator.isNotNull(loginUser))
{
String userName = loginUser.getUsername();
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
// 记录用户退出日志
AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));
}
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.HTTP_OK, "退出成功")));
}
}