一、Controller层
@PostMapping("/login")
public Result login(@RequestBody User user){
//此处用了预先定义好的统一异常处理类 (这步检查字符串是否为空可以省略)
if(!StringUtils.hasText(user.getUserName())){
throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
}
return this.LoginService.login(user);
}
二、Service层
我们从AuthenticationManager接口的实现类作为入口启动认证,那么就要在SecurityConfig配置类中去写个@Bean注解,这样我们才能自动注入。
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
service层
@Service
public class LoginServiceImpl extends ServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache; //稍微封装了一下RedisTemplate
@Override
public Result login(User user) {
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
// step1:调用authenticationManager的authenticate方法,但是该方法的参数
// 是Authentication类的,因此在前面定义一个UsernamePasswordAuthenticationToken的对象
// (UsernamePasswordAuthenticationToken是一个Authentication的实现类)。
// authenticate方法会自动调用UserDetailsService接口的loadUserByUsername方法,
// 所以我们要写一个它的实现类(见下一段代码~)
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 判断返回结果是否为空
if (Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
// step2:获取userId
LoginUser loginUser=(LoginUser) authenticate.getPrincipal(); //强转
String userId=loginUser.getUser().getId().toString(); //Long类型的Id转换成字符串
// step3:生成token,用的JwtUtil是自定义的工具类
String token = JwtUtil.createJWT(userId);
// step4:把用户信息存入Redis
redisCache.setCacheObject("bloglogin:"+userId, loginUser);
// 返回的话肯定还是JSON格式的数据
UserInfoVo userInfoVo= BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
UserLoginVo userLoginVo=new UserLoginVo(token,userInfoVo);
return Result.okResult(userLoginVo);
}
}
定义一个UserDetailsService接口的实现类,重写loadUserByUsername方法(service层的AuthenticationManager类做校验时会自动调用该方法),那么它的返回是UserDetails接口的实现类,所以我们再写个LoginUser作为实现类
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查sql
LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(queryWrapper);
//如果用户查不到,抛出异常,这里先随便抛个Runtime的异常(抛出异常,方法就结束了)
if(Objects.isNull(user)){
throw new RuntimeException("用户不存在");
}
//TODO 后台系统还要查询权限
return new LoginUser(user); //返回的是UserDetails 接口的实现类
}
}
--------------------------------------------------------------------------------------------------------------
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user; //有参构造,直接new实例时传个User对象进去
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.user.getPassword();
}
@Override
public String getUsername() {
return this.user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
好,到这边准备工作做完了,我们再回过头看service层。根据userId生成token,然后把用户信息存入redis,而前端ajax请求的结果里则包含了token值。
接下来定义token验证过滤器,这样子的话,如果前端发的请求是需要验证身份的,那就会走这个过滤器的校验流程。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1、获取请求头携带的token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){
//不需要token的路由可以直接放行
filterChain.doFilter(request,response);
return;
}
//2、解析出userId
Claims claims = null;
try {
claims = JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
//token超时或者非法
Result result = Result.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));//返回一个JSON数据,但是这个方法是void的,所以自定义了一个工具类,方便返回JSON
return;
}
String userId = claims.getSubject();
//3、从redis中获取用户信息
LoginUser loginUser = redisCache.getCacheObject("bloglogin:" + userId);
if(Objects.isNull(loginUser)){
Result result = Result.errorResult(AppHttpCodeEnum.NEED_LOGIN);
WebUtils.renderString(response, JSON.toJSONString(result));
return;
}
//4、如果能返回用户信息,存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response); //放行
}
}
最后,附上springsecurity的配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder(){ //加密处理
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/login").anonymous()
//jwt过滤器测试用,如果测试没有问题吧这里删除了
.antMatchers("/testjwt").authenticated()
// 除上面外的所有请求全部不需要认证即可访问
.anyRequest().permitAll();
http.logout().disable();
//允许跨域
http.cors();
//token验证过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}