在前后端分离中,跳过了UsernamePasswordAuthenticationFilter
,直接在接口中调用AuthenticationManager
的authenticate
方法进行校验
1、Spring Security自带登录页面,而前后端分离项目不需要,我们只需要提供登录接口就可以了
2、登录成功应该响应token
而不是Session+cookie
3、不再是传统ssm中的校验逻辑,而是需要我们自己写逻辑去校验
<!-- springboot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring-security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- mybatis-plus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
创建用户表(user)以及用户表的CRUD,使用MyBatisPlus,我这里user的用户名字段是email
实现UserDetail,对登录实体的方法进行实现
@Data
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.user.getPassword();
}
@Override
public String getUsername() {
return this.user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
实现UserDetailsService,编写查询逻辑
@Service
public class LoginUserDetailService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
// 此处就可以从数据库根据userName查询用户信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("email",userName);
User user = userMapper.selectOne(queryWrapper);
System.out.println(user);
if (Objects.isNull(user)){
throw new UsernameNotFoundException("用户名不存在");
}
// 此处查询用户的权限
// 将查询到的用户封装进LoginUser
return new LoginUser(user);
}
}
在SecurityConfig中配置密码加密器和注入AuthenticationManager
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 注入bcrypt对密码进行加密
* @return
*/
@Bean
public PasswordEncoder setPasswordEncoder(){
return new BCryptPasswordEncoder(10);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
实现登录接口
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
public ResultVO login(@RequestBody User user){
try {
String token = userService.login(user);
return ResultVO.success("登录成功",token);
}catch (BadCredentialsException e){
return ResultVO.failed("邮箱或密码错误,请重新输入!");
}catch (LockedException e){
return ResultVO.failed("账号被锁定,请联系管理员进行处理!");
}catch (DisabledException e){
return ResultVO.failed("用户已失效,请联系管理员进行处理!");
}catch (Exception e){
e.printStackTrace();
return ResultVO.failed("登陆失败,服务器异常!");
}
}
}
@Service
public class UserService {
@Autowired
private AuthenticationManager authenticationManager;
public String login(User user) {
// AuthenticationManager进行用户认证
// 创建一个认证对象,第一个参数是用户名,第二个参数是密码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getEmail(),user.getPassword());
//manager的认证方法返回值也是一个认证对象,如果认证失败会抛出异常
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//认证成功的话,可以从返回的认证对象中获取LoginUser
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
//创建一个永久的jwt给用户,自己在redis中维护用户名对应jwtid,用于续签和登出
String jwtId = UUID.randomUUID().toString();
String token = JwtUtil.createToken(loginUser.getUsername(), -1, jwtId);
//将token存入Redis
RedisFactory.select(0).opsForValue().set(loginUser.getUsername(),jwtId);
return token;
}
}
在SecurityConfig中配置登录路径不拦截
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() //这个非常重要,关闭使用session
.csrf().disable() //关闭csrf
.authorizeRequests() // 认证请求
.antMatchers("/user/login").permitAll() // 定义此路径所有人都可以访问
.anyRequest().authenticated(); // 任何请求都需要身份验证
}
}
编写Token认证过滤器,对已登录用户的token进行认证,注意:这里建议继承框架提供的OncePerRequestFilter
,避免一个请求多次过滤
@Component
public class TokenAuthFilter extends OncePerRequestFilter {
@Autowired
private RoleMapper roleMapper;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 从请求头中获取jwt
String authToken = httpServletRequest.getHeader("auth-token");
// 如果请求头没有token,直接放行,交给下面的过滤器处理
// 如果当前请求的接口需要认证或权限,下面的过滤器会拦截
if (Objects.isNull(authToken) || !StringUtils.hasText(authToken)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
// 验证token,以及redis中是否存在该token
if (JwtUtil.verifyToken(authToken)){
Map<String, String> payload = JwtUtil.getPayload(authToken);
String id = payload.get("id");
String sub = payload.get("sub");
String s = RedisFactory.select(0).opsForValue().get(sub);
// 验证jwt中的id和redis中的id是否一致
if (!Objects.isNull(s) && s.equals(id)) {
//完成续签,刷新token的有效时间
RedisFactory.select(0).expire(sub,UserService.JWTID_LIVE, TimeUnit.SECONDS);
//获取用户权限信息
List<String> list = roleMapper.selectUserRolesByEmail(sub);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String authority : list){
authorities.add(new SimpleGrantedAuthority(authority));
}
// 创建一个认证对象,第一个参数为用户信息,第三个参数为用户权限
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(sub,null,authorities);
// 传入认证对象到SecurityContextHolder中,证明该用户已经认证
SecurityContextHolder.getContext().setAuthentication(token);
}
}
// 放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
在SecurityConfig中配置Token过滤器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TokenAuthFilter tokenAuthFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() //这个非常重要,关闭使用session
.csrf().disable() //关闭csrf
.authorizeRequests() // 认证请求
.antMatchers("/user/login").permitAll() // 定义此路径所有人都可以访问
.anyRequest().authenticated(); // 任何请求都需要身份验证
// 配置在内置用户名密码认证过滤器之前
http.addFilterBefore(tokenAuthFilter, UsernamePasswordAuthenticationFilter.class);
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 退出登录
* @return
*/
@PostMapping("/logout")
public ResultVO logout(){
try {
userService.logout();
return ResultVO.success("退出登录成功");
}catch (Exception e){
return ResultVO.failed("退出登录失败,服务器异常");
}
}
}
@Service
public class UserService {
/**
* 退出登录
*/
public void logout() {
// 在token认证过滤器里面已经将用户信息存入了SecurityContextHolder
// 从SecurityContextHolder取出用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 我们在过滤器中存入的是邮箱,所以获取也是邮箱
String email = (String) authentication.getPrincipal();
RedisFactory.select(0).delete(email);
}
}
思路就是,开启注解,然后在Token认证过滤器中,将用户权限查出来,放到UsernamePasswordAuthenticationToken
的第三个参数里就可以了
这里以邮件验证码为例
可以仿照原生的UsernamePasswordAuthenticationFilter
来写
注意:如果是前后端分离项目,由于验证是自己在service中手动调用、跳过了过滤器,也就不需要这个过滤器
public class EmailCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
/**
* 表示拦截/user/loginByEmail地址的post请求
*/
public EmailCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/user/loginByEmail","POST"));
}
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
/**
* 从请求中获取参数,封装为EmailCodeAuthenticationToken然后调用authenticate方法进行认证
* @param request
* @param response
* @return
* @throws AuthenticationException
* @throws IOException
* @throws ServletException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//如果不是post请求,直接异常
if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("请求方式有误: " + request.getMethod());
}
//如果请求的参数格式不是json,直接异常
if (!request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
throw new AuthenticationServiceException("参数不是json:" + request.getMethod());
}
//从请求体中获取参数
String email = "";
String code = "";
try {
Map<String,String> map = JSONObject.parseObject(String.valueOf(request.getInputStream()),Map.class);
email = map.get("email");
code = map.get("code");
} catch (IOException e) {
throw new AuthenticationServiceException("参数不对:" + request.getMethod());
}
email = email.trim();
//封装为EmailCodeAuthenticationToken
EmailCodeAuthenticationToken token = new EmailCodeAuthenticationToken(email,code);
//进行验证
return this.getAuthenticationManager().authenticate(token);
}
}
可以仿照原生的UsernamePasswordAuthenticationToken
来写,这个类主要封装了验证链过程中需要的主体、是否验证通过等信息
public class EmailCodeAuthenticationToken extends AbstractAuthenticationToken {
/**
* email地址
*/
private final Object email;
/**
* 验证码
*/
private final Object code;
public EmailCodeAuthenticationToken(Object email, Object code) {
super(null);
this.email = email;
this.code = code;
super.setAuthenticated(false);
}
public EmailCodeAuthenticationToken(Object email, Object code,Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.email = email;
this.code = code;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.code;
}
@Override
public Object getPrincipal() {
return this.email;
}
}
可以仿照原生的DaoAuthenticationProvider
来写
这个类有两个方法
authenticate
这个方法接收一个Authentication
的参数,用来对传入的主体信息进行验证
supports
这个方法,主要用于框架分析不同的Authentication
实现类,使用哪个Provider
进行验证
public class EmailCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public EmailCodeAuthenticationProvider(UserDetailsService service){
this.userDetailsService = service;
}
/**
* 验证码认证逻辑
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
EmailCodeAuthenticationToken token = (EmailCodeAuthenticationToken) authentication;
//从token中获取email和验证码code
String email = (String) token.getPrincipal();
String code = (String) token.getCredentials();
//从redis中查询该用户的验证码
String s = RedisFactory.select(1).opsForValue().get(email);
if (Objects.isNull(s) || !code.equals(s)){
throw new BadCredentialsException("验证码错误");
}
// 验证成功从redis删除验证码保证安全
RedisFactory.select(1).delete(email);
// 验证成功就调用我们自己写的LoginUserDetailService来查询用户的相关信息
LoginUser loginUser = (LoginUser) userDetailsService.loadUserByUsername(email);
// 将用户信息封装到EmailCodeAuthenticationToken中返回
EmailCodeAuthenticationToken result = new EmailCodeAuthenticationToken(loginUser,null, loginUser.getAuthorities());
return result;
}
/**
* 判断是上面 authenticate 方法的 authentication 参数,是哪种类型
* 如果是EmailCodeAuthenticationToken就使用该provider进行验证
* @param aClass
* @return
*/
@Override
public boolean supports(Class<?> aClass) {
return (EmailCodeAuthenticationToken.class.isAssignableFrom(aClass));
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginUserDetailService loginUserDetailService;
/**
* 配置自定义验证逻辑
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(loginUserDetailService).passwordEncoder(setPasswordEncoder());
auth.authenticationProvider(emailCodeAuthenticationProvider());
}
/**
* 注入bcrypt对密码进行加密
* @return
*/
@Bean
public PasswordEncoder setPasswordEncoder(){
return new BCryptPasswordEncoder(10);
}
/**
* 注入邮件验证码登录认证provider
* @return
*/
@Bean
public AuthenticationProvider emailCodeAuthenticationProvider(){
EmailCodeAuthenticationProvider provider = new EmailCodeAuthenticationProvider(loginUserDetailService);
return provider;
}
}
使用的时候和上面账号密码验证的login
方法一样,只不过传入authenticationManager.authenticate()
方法的认证对象应该使用自定义的EmailCodeAuthenticationToken
类型