1、准备工作
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!--junit依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
2、数据库校验用户
从之前的分析我们可以知道,我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。
2.1、引入MybatisPuls和mysql驱动的依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
2.2、配置数据库信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/why_security?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 820820
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
server:
port: 8888
定义Mapper接口,修改Uesr实体类,配置Mapper扫描
3、核心代码实现
创建一个类实现UserDetailsService接口,重写其中的方法。更新用户名从数据库中查询用户信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 认证流程图5.1:查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(queryWrapper);
// 如果没有查询到用户就抛出异常
if (Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
// 认证流程图5.1:查询对应权限信息
//封装成UserDetails对象返回
return new LoginUser(user); // // 因为UserDetails是接口,所以返回的应为 它的实现类
}
}
因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。
@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();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
SecurityConfig.java中配置密码加密存储
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
4、自定义登录接口
4.1、接下来需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
@RestController
public class LoginController {
@Autowired
private LoginServcie loginServcie;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
return loginServcie.login(user);
}
}
对登录接口进行放行,在SecurityConfig.java中配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问。即对接口进行放行
.antMatchers("/user/login").anonymous()
.antMatchers("/testCors").hasAuthority("system:test:list") // 基于配置的权限控制
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
4.2、在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证(认证流程第2步),所以需要在SecurityConfig.java中配置把AuthenticationManager注入容器。
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
4.3、认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisCache redisCache;
@Override
public ResponseResult login(User user) {
// 认证流程图第2步:AuthenticationManager authenticate进行用户认证。用户名和密码封装成authenticationToken对象
// 使用Authentication的实现类
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
// 认证流程图第3步:调用AuthenticationManager的authenticate方法进行认证
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 如果认证没通过,给出对应的提示
if (Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
// 如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userid = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userid);
HashMap<String, String> map = new HashMap<>();
map.put("token", jwt);
// 把完整的用户信息存入redis userid作为key
redisCache.setCacheObject("login:"+userid, loginUser);
return new ResponseResult(200, "登录成功", map);
}
}
5、定义jwt过滤器
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder。
@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 = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
// token不存在 放行。因为后面还有其它过滤器
// 如FilterSecurityInterceptor会判断是否是 认证状态,若不是则会抛出异常并给出相应的提示
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
// 解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
//token超时 或 token 非法
throw new RuntimeException("token非法");
}
// 从redis中获取用户信息
String RedisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(RedisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
配置过滤器。因为要指定这个过滤器的位置
SecurityConfig.java中配置。
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//把token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
6、退出登录
只需要定义一个登陆接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可
@RequestMapping("/user/logout")
public ResponseResult logout(){
return loginService.logout();
}
@Override
public ResponseResult logout() {
// 获取SecurityContextHolder中的用户id
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
// 删除redis中的值
redisCache.deleteObject("login:"+userid);
return new ResponseResult(200, "注销成功");
}
在SecurityConfig.java中要关闭默认的退出登录功能。并且要配置我们的退出登录接口需要认证才能访问