为 User 创建 mapper
代码清单:/mapper/UserMapper.java
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
在入口类中对该 mapper 执行扫描
@SpringBootApplication
@MapperScan("com.zhiller.sangengsecurity.mapper")
public class SanGengSecurityApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(SanGengSecurityApplication.class, args);
}
}
该实现类实现了 UserDetailsService
,通过条件查询从 mysql 中找到对应用户
如果用户不存在,抛出异常
如果用户存在,返回一个 UserDetails 对象
代码清单:/service/UserDetailsServiceImpl.java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName,username);
User user = userMapper.selectOne(wrapper);
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//TODO 根据用户查询权限信息 添加到LoginUser中
//封装成UserDetails对象返回
return new LoginUser(user);
}
}
该实体类继承了 UserDetails
@AllArgsConstructor
注解可以自动生成带参数的构造函数,因为我们已经在该类中设置了私有变量 User,所以生成的构造函数就会自带形参 user,然后对应 UserDetailsServiceImpl 中的末尾返回的就是一个 UserDetials 对象
默认的 LoginUser 对象仅需要两个参数:userid 和 password
代码清单:/domain/LoginUser.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
// 下面的这四个玩意必须设置成true,否则不给你验证!!!
@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/LoginService.java
public interface LoginService {
ResponseResult login(User user);
ResponseResult logout();
}
实现登录与登出服务
由于内容过多,具体代码功能查看下方注释,十分详细
代码清单:/service/LoginServiceImpl.java
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
// AuthenticationManager对象,用于处理认证操作
// authenticate()方法会触发Spring Security进行认证,返回一个Authentication对象authenticate,表示认证成功
// 如果认证失败,即authenticate为null,则抛出RuntimeException异常
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if(Objects.isNull(authenticate)){
throw new RuntimeException("用户名或密码错误");
}
//使用userid生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
//authenticate存入redis
redisCache.setCacheObject("login:"+userId,loginUser);
//把token响应给前端
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
return new ResponseResult(200,"登陆成功",map);
}
@Override
public ResponseResult logout() {
// 通过SecurityContextHolder获取当前登录用户的认证信息Authentication
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 认证信息中的主体对象转换为LoginUser对象
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
// 删除redis数据库中的对应对象
redisCache.deleteObject("login:"+userid);
return new ResponseResult(200,"退出成功");
}
}
代码清单:/controller/LoginController.java
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user) {
return loginService.login(user);
}
@RequestMapping("/user/logout")
public ResponseResult logout() {
return loginService.logout();
}
}
配置 JWT 过滤器,实现用户 JWT 校验
代码清单:/filter/JwtAuthenticationTokenFilter.java
// 继承自OncePerRequestFilter,它是Spring提供的一个过滤器基类,确保每个请求只被过滤一次
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
// 实现了具体的过滤逻辑
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
// 解析token
// 因为JWT中的Subject存储的就是userid,JWT解析后可以取出来放入redis进行比对
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
// 存入SecurityContextHolder,便于后续logout方法直接从这里面取出当前登录的用户信息
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
配置 security 基本属性
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// security自带的BCryptPasswordEncoder来对用户密码进行加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 自动装配JWT过滤器
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//把token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}