登录校验流程
spring security认证流程
- 浏览器用户提交用户名和密码。
- 将请求信息封装成Authentication,实现类为UsernamePasswordAuthenticationToken。
- authenticate()方法认证。
- AuthenticationManager委托认证authenticate()。
- DaoAuthenticationProvider通过loadUserByUsername方法获取用户信息。
- 返回UserDetails。
- 通过PasswordEncoder对比UserDetails中的密码与Authentication中密码是否一致。
- 填充Authentication对象,内容为权限信息。
- 返回给 UsernamePasswordAuthenticationFilter一个Authentication对象。
- 将Authentication对象放到SecurityContextHolder中。
部分代码需要重写实现业务
红色需要自己实现的逻辑
- 需要查我们自己的数据库校验用户名密码,那么我们需要实现UserDetailsService接口。
- 在service层,我们编写自己的逻辑方法,需要调用AuthenticationManager类实现认证。
- 继承spring security的WebSecurityConfigurerAdapter类来实现自己的配置类,配置spring security过滤哪些url等。WebSecurityConfigurerAdapter类在tomcat启动后,托管给spring加载。
动手实现
- 实现自己数据库校验,重写UserDetailsService
@Service
public class LoginUserDetailsImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 重写loadUserByUsername方法
* 查询自己数据库中用户信息
* @param username
* @return
* @throws UsernameNotFoundException
*/
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StrUtil.isBlank(username)) {
throw new CommonException("用户" + username + "不存在");
}
/**
* 查询用户信息,认证
*/
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("user_name", username);
User user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)) {
throw new CommonException("该用户不存在");
}
UserBO userBO = new UserBO();
BeanUtil.copyProperties(user, userBO);
CustomUserDetailBO customUserDetailBO = new CustomUserDetailBO(userBO);
return customUserDetailBO;
}
}
- 实现自己登录服务接口
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService userService;
@PostMapping("/login")
public ResultModel<String> login(@RequestBody UserBO userBO) throws CommonException {
return userService.login(userBO);
}
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private UserMapper userMapper;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisUtil redisUtil;
@Override
public ResultModel<String> login(UserBO userBO) throws CommonException {
/**
* 1. 获取授权信息
*/
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userBO.getUserName(), userBO.getPassword());
Authentication authentication = authenticationManager.authenticate(token);
if (Objects.isNull(authentication)) {
throw new CommonException("登录失败");
}
/**
* 获取授权信息中的用户信息
*/
CustomUserDetailBO customUserDetailBO = (CustomUserDetailBO) authentication.getPrincipal();
String userIdStr = customUserDetailBO.getUserBO().getUserId().toString();
/**
* 用户登录,认证成功,那么生成JWT,下次直接使用JWT访问
*/
String jwt = JwtUtil.createJWT(userIdStr);
/**
* 将用户信息存储到redis
*/
redisUtil.setCacheObject("token:"+ userIdStr, customUserDetailBO);
ResultModel<String> resultModel = new ResultModel<>();
resultModel.setMsg("登录成功");
resultModel.setRes(jwt);
return resultModel;
}
}
- 配置spring security,过滤不需要的url
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置过滤的路径等
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
.and()
.authorizeRequests()//csrf关闭
.antMatchers("/user/**").permitAll()//放行
.anyRequest().authenticated()//其他路径拦截
.and()
.formLogin().permitAll()//表单提交放行
;
}
/**
* 注册解码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 暴露AuthenticationManager这个Bean
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
}
- 测试
编写配置过滤器,拦截token
思路:过滤器在每一个请求到达Controller前,都会先经过Filter过滤,这时我们在过滤器里面获取其是否传递了token。
情况1:如果发现token正确,我们直接使用HttpServletResponse返回。
情况2:如果token不存在,那么我们交由后面的数据库查询检查。
情况3:如果redis中不存在token,那么说明redis中token过期或者logout登出删除了redis中的token,那么直接HttpServletResponse返回,提示其重新登录。
自定义过滤器:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, CommonException {
// 获取token
String token = request.getHeader("token");
if (StrUtil.isBlank(token)) {
filterChain.doFilter(request, response);
return;
}
// 解析token
String userId = (String) JwtUtil.parseJWT(token).get("userId");
// 从redis获取用户信息
CustomUserDetailBO customUserDetailBO = redisCache.getCacheObject("token:" + userId);
ResultModel<String> resultModel = new ResultModel<>();
resultModel.setTime(TimeUtil.getNowTime());
if (Objects.isNull(customUserDetailBO)) {
response.setStatus(500);
resultModel.setCode(500);
resultModel.setSuccess(false);
resultModel.setMsg("用户未登陆");
HttpUtil.setRespBody(response, resultModel);
return;
}
// 存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
customUserDetailBO, null, customUserDetailBO.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
/*response.setStatus(200);
resultModel.setMsg("用户登录成功");
HttpUtil.setRespBody(response,resultModel);*/
filterChain.doFilter(request, response);
}
}
配置:
测试:
授权实现
UserDetailService查询数据库权限:
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StrUtil.isBlank(username)) {
throw new CommonException("用户" + username + "不存在");
}
/**
* TODO 查询用户信息,认证
*/
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("user_name", username);
User user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)) {
throw new CommonException("该用户不存在");
}
UserBO userBO = new UserBO();
BeanUtil.copyProperties(user, userBO);
/**
* TODO 查询用户的权限信息,授权
*/
List<Role> roleList = roleMapper.selectRolesByUserId(user.getUserId());
List<RoleBO> roleBOList = roleList.stream().map(role -> {
RoleBO roleBO = new RoleBO();
BeanUtils.copyProperties(role, roleBO);
List<Menu> menuList= menuMapper.selectMenuByRoleId(role.getRoleId());
List<MenuBO> menuBOList = menuList.stream().map(menu -> {
MenuBO menuBO = new MenuBO();
BeanUtils.copyProperties(menu, menuBO);
log.info("menuBO => {}", menuBO.getMenuCode());
return menuBO;
}).collect(Collectors.toList());
roleBO.setMenuBOList(menuBOList);
return roleBO;
}).collect(Collectors.toList());
return new CustomUserDetailBO(userBO, roleBOList);
}
为url分配权限:
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
.and()
.authorizeRequests()//csrf关闭
.antMatchers("/user/r/r1").hasAuthority("01")
.antMatchers("/user/r/r2").hasAuthority("02")
.antMatchers("/user/r/r3").denyAll()
.antMatchers("/user/**").permitAll()//放行
.anyRequest().authenticated()
.and()
.formLogin().permitAll()//表单提交放行
.and()
.csrf().disable()
;
}
/**
* 注册解码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 暴露AuthenticationManager这个Bean
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
}
编写CustomUserDetailBO类:
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities != null){
return authorities;
}
authorities = new ArrayList<>();
//把permission中String类型的权限信息封装成SimpleGrantedAuthority对象
roleBOList.stream().forEach(roleBO -> {
roleBO.getMenuBOList().forEach(menuBO -> {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(menuBO.getMenuCode());
authorities.add(authority);
});
});
return authorities;
}
过滤器修改:当用户登录成功,为其设置应该提供的权限:
// 存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
customUserDetailBO, null, customUserDetailBO.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
登出功能
思路:通过SecurityContextHolder类获取Authentication(里面封装了我们的登录信息),然后通过Authentication里面存储的User对象的用户id,然后拼接成redis的key获取redis中的对象,将其删除,这样下次拿着token来查询redis对象,发现没有了,就会提示其重新登录。
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private ILogoutService logoutService;
@GetMapping("/logout")
public ResultModel<String> logout() throws CommonException {
return logoutService.logout();
}
}
@Service
public class LogoutServiceImpl implements ILogoutService {
@Autowired
private RedisUtil redisUtil;
@Override
public ResultModel<String> logout() throws CommonException {
UsernamePasswordAuthenticationToken authentication
= (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
CustomUserDetailBO customUserDetailBO = (CustomUserDetailBO) authentication.getPrincipal();
Long userId = customUserDetailBO.getUserBO().getUserId();
redisUtil.deleteObject("token:" + userId);
ResultModel<String> resultModel = new ResultModel<>();
resultModel.setTime(TimeUtil.getNowTime());
resultModel.setMsg("用户注销成功");
resultModel.setRes(String.valueOf(userId));
return resultModel;
}
}
网友评论