【若依框架】登录,token,自定义session,鉴权等前后端流程解读

【若依框架】登录,token,自定义session,鉴权等前后端流程解读

背景

之前虽然讲了login,getInfo,getRoutes的三个接口,但从设计的角度来讲,这3个接口并没有完整实现一个功能。这里重点讲解若依框架对于自定义session,token校验,权限验证三个方面的实现。这些对于自己实现一个简单的后端框架有不错的参考意义

功能说明

登录功能\login及token的生成权限过滤校验自定义session前端如何配合

可以参考上一篇博客

登录及token生成

主要解决的是用户登录、生成token和session的场景

前端

用户登录页输入username,password,code(验证码),提交login接口

注:省略获取验证码接口和rememberMe的cookie使用

SysLoginController

# 验证码验证

# 2 登录验证

// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername

authentication = authenticationManager

.authenticate(new UsernamePasswordAuthenticationToken(username, password));

注:上面会把LoginUser注入到Security的Principal中

# 3 生成token

LoginUser loginUser = (LoginUser) authentication.getPrincipal();

// 生成token

return tokenService.createToken(loginUser);

TokenService

生成token包含了自定义session和返回token两大部分

使用UUID作为sessionId

由于作者使用了token这个命名会给阅读带来很大困扰,这里我们理解为sessionId是最准确的。

public String createToken(LoginUser loginUser)

{

#这里的token准确的说法是sessionID

String token = IdUtils.fastUUID();

loginUser.setToken(token);

setUserAgent(loginUser);

refreshToken(loginUser);

Map claims = new HashMap<>();

claims.put(Constants.LOGIN_USER_KEY, token);

return createToken(claims);

}

以sessionId为key,LoginUser为Value保存redis,这个过程本质上就是自定义session。

注:忽略用户信息填充的过程。

/**

* 刷新令牌有效期

*

* @param loginUser 登录信息

*/

public void refreshToken(LoginUser loginUser)

{

loginUser.setLoginTime(System.currentTimeMillis());

loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);

// 根据uuid将loginUser缓存

String userKey = getTokenKey(loginUser.getToken());

redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);

}

生成JwtToken

这里产生了真正的Jwttoken返回给了前端

没有过期时间保存了sessionId,方便关联session

private String createToken(Map claims)

{

String token = Jwts.builder()

.setClaims(claims)

.signWith(SignatureAlgorithm.HS512, secret).compact();

return token;

}

总结:

验证验证码验证登录信息生成sessionId以sessionId为key,保存LoginUser的redis,有效期为30分钟生成包含sessionId的token返回给前端JwtToken

注意4,用户登录后在redis实现了自定义session,有效期为expireTime。如果redis中的sessionId过期了,就代表用户session失效。淡然,如果token不对会跑出校验异常,也无法通过,接下来会详细介绍。

接口请求时鉴权过滤

安全配置

这里配置了可以匿名登录的基本信息

AuthenticationEntryPointImpl 认证失败处理类LogoutSuccessHandlerImpl 登出处理类JwtAuthenticationTokenFilter权限过滤器,核心

/**

* spring security配置

*

* @author ruoyi

*/

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

public class SecurityConfig extends WebSecurityConfigurerAdapter

{

/**

* 自定义用户认证逻辑

*/

@Autowired

private UserDetailsService userDetailsService;

/**

* 认证失败处理类

*/

@Autowired

private AuthenticationEntryPointImpl unauthorizedHandler;

/**

* 退出处理类

*/

@Autowired

private LogoutSuccessHandlerImpl logoutSuccessHandler;

/**

* token认证过滤器

*/

@Autowired

private JwtAuthenticationTokenFilter authenticationTokenFilter;

/**

* 跨域过滤器

*/

@Autowired

private CorsFilter corsFilter;

/**

* 解决 无法直接注入 AuthenticationManager

*

* @return

* @throws Exception

*/

@Bean

@Override

public AuthenticationManager authenticationManagerBean() throws Exception

{

return super.authenticationManagerBean();

}

/**

* anyRequest | 匹配所有请求路径

* access | SpringEl表达式结果为true时可以访问

* anonymous | 匿名可以访问

* denyAll | 用户不能访问

* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)

* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问

* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问

* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问

* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问

* hasRole | 如果有参数,参数表示角色,则其角色可以访问

* permitAll | 用户可以任意访问

* rememberMe | 允许通过remember-me登录的用户访问

* authenticated | 用户登录后可访问

*/

@Override

protected void configure(HttpSecurity httpSecurity) throws Exception

{

httpSecurity

// CSRF禁用,因为不使用session

.csrf().disable()

// 认证失败处理类

.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

// 基于token,所以不需要session

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

// 过滤请求

.authorizeRequests()

// 对于登录login 验证码captchaImage 允许匿名访问

.antMatchers("/login", "/captchaImage").anonymous()

.antMatchers(

HttpMethod.GET,

"/*.html",

"/**/*.html",

"/**/*.css",

"/**/*.js"

).permitAll()

.antMatchers("/profile/**").anonymous()

.antMatchers("/common/download**").anonymous()

.antMatchers("/common/download/resource**").anonymous()

.antMatchers("/swagger-ui.html").anonymous()

.antMatchers("/swagger-resources/**").anonymous()

.antMatchers("/webjars/**").anonymous()

.antMatchers("/*/api-docs").anonymous()

.antMatchers("/druid/**").anonymous()

// 除上面外的所有请求全部需要鉴权认证

.anyRequest().authenticated()

.and()

.headers().frameOptions().disable();

httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);

// 添加JWT filter

httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

// 添加CORS filter

httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);

httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);

}

/**

* 强散列哈希加密实现

*/

@Bean

public BCryptPasswordEncoder bCryptPasswordEncoder()

{

return new BCryptPasswordEncoder();

}

/**

* 身份认证接口

*/

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception

{

auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());

}

}

权限过滤JwtAuthenticationTokenFilter权限过滤器

这是一个过滤器,所有接口请求都会经过这个过滤器(包括login)

@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);

}

}

getLoginUser

获取请求头中的Authorization字段的tokenparseToken验证token有效性,如果token不对这里会抛出异常解析token获取token中的sessionId信息根据sessionId获取LogInUser并返回

/**

* 获取用户身份信息

*

* @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;

}

authenticationToken

刷新token和session校验用户信息更新用户信息到SpringSecurity的上下文

总结:

校验jwtToken更新session更新SpringSecurity上下文

用户态(session管理)

上面其实多处涉及了session,这个解决了请求时获取当前登录用户的问题。

我们可以在任意地方通过下面方法获取当前请求的用户信息,并存放的LogInUser中供业务使用

LoginUser loginUser = tokenService.getLoginUser(request)

前端逻辑梳理

要求读者对于前端也有一定基础,可以顺着下面过程大概看一下前端的主逻辑

login.vue

填写信息点击登录时调用下面方法

参数校验设置cookiethis.$store.dispatch 使用了vuex的功能,调用store中的Login方法。这里是前端逻辑跳跃store中的user.js有Login方法,请求了后端login接口,如果成功就返回token

handleLogin() {

this.$refs.loginForm.validate(valid => {

if (valid) {

this.loading = true;

if (this.loginForm.rememberMe) {

Cookies.set("username", this.loginForm.username, { expires: 30 });

Cookies.set("password", encrypt(this.loginForm.password), { expires: 30 });

Cookies.set('rememberMe', this.loginForm.rememberMe, { expires: 30 });

} else {

Cookies.remove("username");

Cookies.remove("password");

Cookies.remove('rememberMe');

}

this.$store.dispatch("Login", this.loginForm).then(() => {

this.$router.push({ path: this.redirect || "/" }).catch(()=>{});

}).catch(() => {

this.loading = false;

this.getCode();

});

}

});

登录接口

成功返回是获取到token保存token到cookie保存store中token字段全局变量

// 登录

Login({ commit }, 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 => {

let data = res.data

setToken(data.access_token)

commit('SET_TOKEN', data.access_token)

setExpiresIn(data.expires_in)

commit('SET_EXPIRES_IN', data.expires_in)

resolve()

}).catch(error => {

reject(error)

})

})

},

前端对于请求的封装

utils.require.js中对于所有请求拦截并添加了token

拦截请求从cookie获取token,这里其实可以从vuex获取,但cookie有持久化效果给所有请求添加请求头Authorization存放token,这部分和后端过滤器对应

// 创建axios实例

const service = axios.create({

// axios中请求配置有baseURL选项,表示请求URL公共部分

baseURL: process.env.VUE_APP_BASE_API,

// 超时

timeout: 10000

})

// request拦截器

service.interceptors.request.use(config => {

// 是否需要设置 token

const isToken = (config.headers || {}).isToken === false

if (getToken() && !isToken) {

config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改

}

// get请求映射params参数

if (config.method === 'get' && config.params) {

let url = config.url + '?';

for (const propName of Object.keys(config.params)) {

const value = config.params[propName];

var part = encodeURIComponent(propName) + "=";

if (value !== null && typeof(value) !== "undefined") {

if (typeof value === 'object') {

for (const key of Object.keys(value)) {

let params = propName + '[' + key + ']';

var subPart = encodeURIComponent(params) + "=";

url += subPart + encodeURIComponent(value[key]) + "&";

}

} else {

url += part + encodeURIComponent(value) + "&";

}

}

}

url = url.slice(0, -1);

config.params = {};

config.url = url;

}

return config

}, error => {

console.log(error)

Promise.reject(error)

})

总结

以上是若依框架对于登录,token,session,鉴权等场景的处理流程。由于使用到了springSecurity让逻辑有跳跃,其实是非常简单使用的设计思路。希望对大家有帮助。

相关推荐

够花贷款好不好下款啊?
365用什么浏览器登录

够花贷款好不好下款啊?

📅 09-08 👁️ 2348
学生请假条范文15
365用什么浏览器登录

学生请假条范文15

📅 09-07 👁️ 6917
乾隆后一个皇帝是谁?
microsoft365版本

乾隆后一个皇帝是谁?

📅 08-06 👁️ 8859