前言
回顾:
【Spring Security】springboot + mybatis-plus + mysql 从数据库读取用户信息验证登录
【Spring Security】springboot + mybatis-plus + mysql 密码加密存储下的数据库用户验证登录
【Spring Security】springboot + mybatis-plus + mysql 用户token验证登录功能
如果使用过spring security的原生/logout接口,可以发现,如果持有之前生效过的token,即使调用/logout的用户登出了,我们还是可以访问需要权限才能调用的其他接口,这是不合理的,用户登出了怎么还可以访问其他接口呢?
因此需要手动试/logout接口调用后能够让用户权限失效,我这里利用了 enabled 字段来控制用户权限。
核心
核心是,令enabled字段成为用户登录的状态字段,当用户login登录时,修改数据表中的用户的enabled字段为true,表示登录状态。
当用户logout登出时,或者token过期时,修改数据表中的用户的enabled字段为false,表示用户已经登出。
@Component
public class UsernamePasswordLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private UserService userService;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();
// 获取当前登录用户名
UserDetails userDetails = (UserDetails) principal;
String username = userDetails.getUsername();
// 用户失效
userService.userDisenabled(username);
// 构造一个 UserPattern 返回体返回
UserPattern userPattern = new UserPattern();
userPattern.setUsername(username);
userPattern.setPassword(userDetails.getPassword());
userPattern.setEnabled(Boolean.FALSE);
userPattern.setAccountNonExpired(userDetails.isAccountNonExpired());
userPattern.setAccountNonLocked(userDetails.isAccountNonLocked());
userPattern.setCredentialsNonExpired(userDetails.isCredentialsNonExpired());
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(userPattern));
out.flush();
out.close();
}
}
由于所有的接口调用都需要通过过滤器进行token鉴定,因此在在JWT过滤器中,增加一个对当前登录用户的判断,如果当前登录用户是enabled=true,则可以继续验证token,如果enabled=false,说明用户已经失效,直接清空token后返回。
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private String tokenHead = "Bearer ";
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = request.getHeader(HttpHeaders.AUTHORIZATION);
if (!StringUtils.hasText(tokenValue)){
filterChain.doFilter(request, response);
return;
}
// 去掉tokenHead留下token
String token = tokenValue.substring(tokenHead.length());
Map<String, Object> parseJWT = JWTUtil.parseToken(token);
// 检查token过期时间
if (JWTUtil.isExpiresIn((long) parseJWT.get("expiresIn"))){
// token 已经过期,令token 销毁,账号失效
SecurityContextHolder.getContext().setAuthentication(null);
filterChain.doFilter(request, response);
return;
}
String username = (String) parseJWT.get("username");
// 检查 user 是否 enabled
if (!userService.isUserEnabled(username)) {
// 用户不可用,token销毁
SecurityContextHolder.getContext().setAuthentication(null);
filterChain.doFilter(request, response);
return;
}
if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null){
// 正常用户
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(userDetails != null && userDetails.isEnabled()){
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置用户登录状态
log.info("authenticated user {}, setting security context", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
实现
UserService.java
package org.sample.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.sample.common.request.UserRegisterRequest;
import org.sample.common.response.Response;
import org.sample.dao.entity.UserPattern;
import java.util.List;
/**
* 用户信息服务接口
* @author
*/
public interface UserService extends IService<UserPattern> {
Response userRegister(UserRegisterRequest userRegisterRequest);
UserPattern loadByUserName(String username);
String isAuthorizedUsersExist(List<String> authorizedUsers);
Response userDisenabled(String username);
Boolean isUserEnabled(String username);
}
UserServiceImpl.java
package org.sample.service.Impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.sample.common.enums.IsDeletedEnum;
import org.sample.common.request.UserRegisterRequest;
import org.sample.common.response.Response;
import org.sample.common.utils.ResponseGenerator;
import org.sample.common.utils.SecurityUtils;
import org.sample.dao.entity.UserPattern;
import org.sample.dao.mapper.UserServiceMapper;
import org.sample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import javax.validation.Valid;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserServiceMapper, UserPattern> implements UserService, UserDetailsService {
@Autowired
private UserServiceMapper userServiceMapper;
@Autowired
private UserService userService;
/**
* 用户注册
* @param userRegisterRequest
* @return
*/
@Override
public Response userRegister(@Valid UserRegisterRequest userRegisterRequest) {
String username = userRegisterRequest.getUsername();
// XSS和SQL注入检查,得到过滤后用户名
String s = SecurityUtils.cleanXSSSql(username);
// 过滤前和过滤后进行比对
if (!s.equals(username)) {
return ResponseGenerator.genFailResponse("非法输入!");
}
// 用户名查重
Integer repeat = userServiceMapper.selectCount(Wrappers.lambdaQuery(UserPattern.class)
.likeRight(UserPattern::getUsername, username)
.likeLeft(UserPattern::getUsername, username)
);
if (repeat > 0) {
return ResponseGenerator.genFailResponse("用户名已存在,不可重复注册");
}
UserPattern userPattern = new UserPattern();
userPattern.setUsername(username);
// 密码加密储存
String encodedPassword = new BCryptPasswordEncoder().encode(userRegisterRequest.getPassword());
userPattern.setPassword(encodedPassword);
userPattern.setEnabled(Boolean.TRUE);
userPattern.setAccountNonExpired(Boolean.TRUE);
userPattern.setAccountNonLocked(Boolean.TRUE);
userPattern.setCredentialsNonExpired(Boolean.TRUE);
userPattern.setIsDeleted(IsDeletedEnum.NO.getCode());
userPattern.setCreateTime(new Timestamp(System.currentTimeMillis()));
userPattern.setCreateUser(username);
userPattern.setUpdateUser(username);
// 用户名落库存储
int insert = userServiceMapper.insert(userPattern);
if (insert > 0) {
return ResponseGenerator.genSuccessResponse(userPattern);
}
return ResponseGenerator.genFailResponse("网络错误!当前服务不可用,请尝试重试或联系客服");
}
/**
* 方法一
* spring security方法:auth.userDetailsService(userDetailsService())
* @param username 用户名
* @return UserPattern
*/
@Override
public UserPattern loadByUserName(String username) {
// XSS和SQL注入检查
String s = SecurityUtils.cleanXSSSql(username);
if (!s.equals(username)) {
throw new RuntimeException("非法输入!");
}
List<UserPattern> userPatterns = userServiceMapper.selectList(Wrappers.lambdaQuery(UserPattern.class)
.likeLeft(UserPattern::getUsername, username)
.likeRight(UserPattern::getUsername, username)
.eq(UserPattern::getIsDeleted, IsDeletedEnum.NO.getCode())
);
UserPattern userPattern = userPatterns.get(0);
return userPattern;
}
/**
* 方法二
* spring security方法:auth.userDetailsService(userDetailsService)
* @param username 用户名
* @return UserDetails
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// XSS和SQL注入检查
String s = SecurityUtils.cleanXSSSql(username);
if (!s.equals(username)) {
throw new RuntimeException("非法输入!");
}
List<UserPattern> userPatterns = userServiceMapper.selectList(Wrappers.lambdaQuery(UserPattern.class)
.likeLeft(UserPattern::getUsername, username)
.likeRight(UserPattern::getUsername, username)
.eq(UserPattern::getIsDeleted, IsDeletedEnum.NO.getCode())
);
try {
// 如果get不到,就说明库里没有该用户,catch扔异常
UserPattern userPattern = userPatterns.get(0);
userPattern.setUpdateTime(new Timestamp(System.currentTimeMillis()));
userPattern.setEnabled(Boolean.TRUE);
userService.updateById(userPattern);
// 如果查不到用户名,这里可以抛出UsernameNotFoundException异常
// 根据username查询权限,这里假设从任意位置查到权限是auth
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("auth"));
return new User(userPattern.getUsername(), userPattern.getPassword(), Boolean.TRUE,
userPattern.isAccountNonExpired(), userPattern.isCredentialsNonExpired(), userPattern.isAccountNonLocked(), authorities);
} catch (Exception e) {
throw new RuntimeException("用户不存在!");
}
}
/**
* 检查输入的 authorizedUsers 在数据库是否存在
* @param authorizedUsers 输入的授权用户
* @return String 如果输入存在,返回空字符串
*/
@Override
public String isAuthorizedUsersExist(List<String> authorizedUsers) {
String result = "";
// 遍历授权用户列表 authorizedUsers
for (String username : authorizedUsers) {
List<UserPattern> userPatterns = userServiceMapper.selectList(Wrappers.lambdaQuery(UserPattern.class)
.likeLeft(UserPattern::getUsername, username)
.likeRight(UserPattern::getUsername, username)
.eq(UserPattern::getIsDeleted, IsDeletedEnum.NO.getCode())
);
try {
// 如果get不到,就说明库里没有该用户,则authorizedUsers中包含无效输入,走catch
UserPattern userPattern = userPatterns.get(0);
} catch (Exception e) {
log.error("输入的授权用户 " + username + " 无效,请重新检查!");
result += username;
return result;
}
}
// 如果 authorizedUsers 存在,返回空字符串
return result;
}
/**
* 使用户失效
* @param username 用户名
* @return Response
*/
@Override
public Response userDisenabled(String username) {
// 把用户取出
List<UserPattern> userPatterns = userServiceMapper.selectList(Wrappers.lambdaQuery(UserPattern.class)
.likeLeft(UserPattern::getUsername, username)
.likeRight(UserPattern::getUsername, username)
.eq(UserPattern::getIsDeleted, IsDeletedEnum.NO.getCode())
);
try {
// 如果get不到,就说明库里没有该用户,catch扔异常
UserPattern userPattern = userPatterns.get(0);
userPattern.setUpdateTime(new Timestamp(System.currentTimeMillis()));
// 用户失效
userPattern.setEnabled(Boolean.FALSE);
userService.updateById(userPattern);
return ResponseGenerator.genSuccessResponse();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return ResponseGenerator.genFailResponse("网咯错误!");
}
/**
* 判断 user 是否 enabled
* @param username
* @return
*/
@Override
public Boolean isUserEnabled(String username) {
// 把用户取出
List<UserPattern> userPatterns = userServiceMapper.selectList(Wrappers.lambdaQuery(UserPattern.class)
.likeLeft(UserPattern::getUsername, username)
.likeRight(UserPattern::getUsername, username)
.eq(UserPattern::isEnabled, Boolean.TRUE)
.eq(UserPattern::getIsDeleted, IsDeletedEnum.NO.getCode())
);
// 如果userPatterns则为空,则user已失效
if (userPatterns.isEmpty()) {
return Boolean.FALSE;
}
return Boolean.TRUE;
}
}
SecurityConfig.java
package org.sample.config;
import org.sample.config.filter.JwtAuthenticationTokenFilter;
import org.sample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
/**
* spring-security 配置类
* @author
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* PasswordEncoder 是密码加密接口,因为我们是循序渐进的,我这里先用无加密实例
* @return PasswordEncoder
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 实现configure(AuthenticationManagerBuilder auth)配置方法,为了图方便,我先在内存中创建默认用户harry,默认密码是123456,角色是user
* 配置文件中的默认用户和默认密码注释掉了
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 通过HttpSecurity 对象,获取到表单登录配置对象,修改对应的用户名和密码参数名称,即可完成自定义用户名和密码参数名称
* .antMatchers("/swagger-ui.html").permitAll() 等等是给swagger开绿灯,不用登录直接访问
* .antMatchers("/register").permitAll() 给用户注册接口开绿灯,注册的时候不需要登录
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/*").permitAll()
.antMatchers("/csrf").permitAll()
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/login.html").permitAll()
.antMatchers("/authentication", "/login.html").permitAll()
.antMatchers("/register").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.permitAll()
.and().csrf().disable()
;
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
public static void main(String[] args) {
}
}
JwtAuthenticationTokenFilter.java
package org.sample.config.filter;
import lombok.extern.slf4j.Slf4j;
import org.sample.common.utils.JWTUtil;
import org.sample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private String tokenHead = "Bearer ";
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private UserService userService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = request.getHeader(HttpHeaders.AUTHORIZATION);
if (!StringUtils.hasText(tokenValue)){
filterChain.doFilter(request, response);
return;
}
// 去掉tokenHead留下token
String token = tokenValue.substring(tokenHead.length());
Map<String, Object> parseJWT = JWTUtil.parseToken(token);
String username = (String) parseJWT.get("username");
// 检查token过期时间
if (JWTUtil.isExpiresIn((Long) parseJWT.get("expiresIn"))){
// 用户失效
userService.disenabledUser(username);
// token 已经过期,令token 销毁,账号失效
SecurityContextHolder.getContext().setAuthentication(null);
filterChain.doFilter(request, response);
return;
}
// 检查 user 是否 enabled
if (!userService.isUserEnabled(username)) {
// 用户不可用,token销毁
SecurityContextHolder.getContext().setAuthentication(null);
filterChain.doFilter(request, response);
return;
}
if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null){
// 正常用户
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(userDetails != null && userDetails.isEnabled()){
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置用户登录状态
log.info("authenticated user {}, setting security context", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
UsernamePasswordLogoutSuccessHandler.java
package org.sample.handler;
import com.alibaba.fastjson.JSON;
import org.sample.dao.entity.UserPattern;
import org.sample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
*
* @author
*/
@Component
public class UsernamePasswordLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private UserService userService;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();
// 获取当前登录用户名
UserDetails userDetails = (UserDetails) principal;
String username = userDetails.getUsername();
// 用户失效
userService.userDisenabled(username);
// 构造一个 UserPattern 返回体返回
UserPattern userPattern = new UserPattern();
userPattern.setUsername(username);
userPattern.setPassword(userDetails.getPassword());
userPattern.setEnabled(Boolean.FALSE);
userPattern.setAccountNonExpired(userDetails.isAccountNonExpired());
userPattern.setAccountNonLocked(userDetails.isAccountNonLocked());
userPattern.setCredentialsNonExpired(userDetails.isCredentialsNonExpired());
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(userPattern));
out.flush();
out.close();
}
}
UserPattern.java
package org.sample.dao.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.sql.Timestamp;
/**
* user用entity
* @author
*/
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "test.image_management_user")
public class UserPattern implements Serializable {
/**
* ID,自增,主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名 userName
*/
@TableField(value = "username")
private String username;
/**
* 密码 password
*/
@TableField(value = "passwd")
private String password;
@TableField(value = "enabled")
private boolean enabled;
@TableField(value = "account_non_expired")
private boolean accountNonExpired;
@TableField(value = "account_non_locked")
private boolean accountNonLocked;
@TableField(value = "credentials_non_expired")
private boolean credentialsNonExpired;
/**
* 是否删除
*/
@TableField(value = "is_deleted")
private int isDeleted;
/**
* 创建时间
*/
@TableField(value = "create_time")
private Timestamp createTime;
/**
* 创建用户
*/
@TableField(value = "create_user")
private String createUser;
/**
* 更新时间
*/
@TableField(value = "update_time")
private Timestamp updateTime;
/**
* 更新用户
*/
@TableField(value = "update_user")
private String updateUser;
}
|