Spring Security是一个用于在Java应用程序中实现身份验证和授权的框架。它提供了一套强大的安全性功能,可帮助您保护您的应用程序免受未经授权的访问和攻击。
接下来就带领大家进行一个SpringSecurity的引入和简单使用
pom依赖引入:
<!--springSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.自定义实现类,实现UserDetailService,
/**
*这个类实现了Sucrity框架的一个接口 UserDetailsService,重写UserDetails方法,
* 将查询出来的User对象存储到UserDetails中(我们自己创建了一个类LoginUserDetails实现了UserDetails接口)
*/
@Service
public class LoginUserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/*
* 这个方法是sucrity框架已经写好的,我们实现UserDetailsService接口重写方法
* 在登录的时候会先走到这一步,我们在这里检验一下用户名是否存在
* */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username",username);
User user = userMapper.selectOne(userQueryWrapper);
if (ObjectUtils.isEmpty(user)){
throw new RuntimeException("登录失败,用户名或密码输入错误");
}
// TODO: 2023/7/17 同时将user存入到redis中
// 这里需要返回UserDetails类型,UserDetails是个接口,我们需要自己创建一个类去实现它
// 将user返回之后,sucrity会自己去比较密码是否正确,注意;sucrity比较的时候是将密码加密之后比较,所以在数据库存储时,需要将密码加密
return new LoginUserDetails(user);
}
}
2. 创建UserLogin类实现UserDetals
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUserDetails implements UserDetails {
/*
* 我们在这里创建一个成员变量,对象为我们的实体类 User,这样有参构造就会有User,下面的各种返回方法,也都是从user中取
* */
private User user;
/**
*这个是返回权限集合的方法
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* 这个是返回用户密码方法
* @return
*/
@Override
public String getPassword() {
return user.getPassword();
}
/**
* 这个是返回用户名方法
* @return
*/
@Override
public String getUsername() {
return user.getUsername();
}
/**
* 下面几个方法改成true
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
截止到这一步,简单的登录校验就做好了,sucrity会根据返回的user自己比较密码,注意密码存储数据库时一定要加密,否则比对不成功。
3. 实现项目的登陆接口
@RestController
@RequestMapping("/cxx")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/userLogin")
public ResponseResult userLogin(@RequestBody User user){
String token = userService.userLogin(user);
return new ResponseResult(200,"登录成功",token);
}
}
以上先创建一个接口,是我们的登录接口,具体业务在impl中
首先我们需要用到Security框架的校验,所以我们需要创建AuthenticationManager对象,这个对象是需要我们在SecurityConfig中创建出来然后交给Spring管理,具体代码如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
// 使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
/*
* 重写AuthenticationManager,并且交给Spring管理
* */
@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("/cxx/userLogin", "/user/save").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
然后再impl中我们使用@Autowired来注入, 详细请看具体的代码,其中我写了注释
// 在配置类中重写方法,然后交给Spring管理,我们在这里直接注入
@Autowired
private AuthenticationManager authenticationManager;
/**
* 登录业务逻辑
* @param user
* @return
*/
@Override
public String userLogin(User user) {
/* 这里我们使用了sucrity框架,我们需要拿到AucesscationManager对象,那么这个对象我们是要交给Spring管理的
我们去SucrityConfiger的配置类中重写然后交给Spring管理
调用authenticationManager的authenticate方法,但是我们发现参数是Authentication类型的,Authentication是一个
接口,我们需要创建它的实现类对象,点进去接口发现他有一个实现类叫UsernamePasswordAuthenticationToken,我们直接new他,
里面的参数分别是用户的用户名和密码
*/
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
// 调用下面这个方法,其实就走到了我们之前自定义的LoginUserService,进行用户名密码判定
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
// 我们只需要判定Authentication对象是否为空,就知道校验有没有成功
if (ObjectUtils.isEmpty(authenticate)){
// 如果为空,说明登录校验失败
throw new RuntimeException("登录失败,请检查用户名或密码");
}
// 如果登录成功,那么就返回给前端jwt(token),为了安全,我们只返回userId
// 那么我们怎么拿到UserId呢,其实我们在LoginUserService里面已经把User信息返回给了Sucrity,在这里可以直接从authenticate拿
LoginUserDetails loginUserDetails = (LoginUserDetails) authenticate.getPrincipal();
User loginUser = loginUserDetails.getUser();
String token = JwtUtil.createJWT(loginUser.getId().toString());
// 三天后过期
redisTemplate.opsForValue().set(loginUser.getId().toString(), loginUserDetails, 3, TimeUnit.DAYS);
return token;
}
截至到这里,登录成功之后我们将用户id用JWT进行了加密,然后返回给前端。
4. 实现项目的Jwt校验,保证系统的安全
前面我们说到了,当用户登录成功之后,我们会把生成的jwt返回给前端,然后前端在每次请求的时候都要带上这个token,
我们用这个token从redis中拿到用户的信息,然后存到SeucrityContextHolder中,这样Seucrity会自己判定请求是否合法
那么具体怎么做我们需要先创建一个自定义的Jwt校验的过滤器,然后把这个过滤器加入到Security的过滤链中。
首先我们创建一个过滤器,注意这个过滤器我们继承OncePerRequestFilter类,具体代码如下
/**
* 这是我们自定义的Jwt过滤器,来校验token是否正确,保证我们的系统安全
*/
@Component
public class MyJwtFilter extends OncePerRequestFilter {
@Resource
private RedisTemplate<String,Object> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 首先我们要拿到前端传递过来的token,token是放在请求头中的
String token = request.getHeader("token");
// 这里我们要判定token是否为空,如果为空,就说明没有登录,或者非法请求
if (ObjectUtils.isEmpty(token)){
// 如果为空我们这里就给放行,不走下面的业务,在Seucrity的框架中后面的过滤器会自己判定
filterChain.doFilter(request,response);
// 记住这里一定要return,因为Security在做完判定在以后,过滤链会再返回走一圈,如果我们这里不return,他就又会走下面的业务代码
return;
}
// 如果token存在,那么我们就解密token并在redis中取出用户信息,存到SeucrutyContext中
try {
Claims claims = JwtUtil.parseJWT(token);
token = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
// 如果这里解析异常报错,就说明token出了问题,我们抛出异常
throw new RuntimeException("token非法");
}
LoginUserDetails loginUserDetails =(LoginUserDetails)redisTemplate.opsForValue().get(token);
if (ObjectUtils.isEmpty(loginUserDetails)){
// 如果redis中没有存到信息,那么说明要么没登录,要么过期了,我们抛出异常即可
throw new RuntimeException("请登录");
} else {
// TODO: 2023/8/1 在redis中进行续期
}
// TODO: 2023/8/1 下面第三个参数是用户的权限信息,暂时我们没有做权限业务,写为null就可以了
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUserDetails.getUser()), null, null);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 到这一步我们就将用户信息存入到SecurityContextHolder中
// 到最后一步一定记得放行
filterChain.doFilter(request,response);
// 到这里我们的Jwt过滤器就写完了,但是Seucrity是怎么知道要先经过我们这个过滤器呢,我们要去Config中配置一下才可以
}
到这里过滤器就已经创建完成,接下来我们需要把过滤器加入到Seucrity的过滤链中,我们需要在SeucrityConfig中添加过滤器在指定的位置,具体代码如下:
// 我们自定义的过滤器是要放在最前面的,就放在UsernamePasswordAuthenticationFilter这个过滤器之前
http.addFilterBefore(myJwtFilter, UsernamePasswordAuthenticationFilter.class);
截至这里我们测试,登录成功之后,拿到返回的token,将token放在请求头中,成功访问,如果没有token就说明请求非法
5.实现退出系统接口
思路就是把当前请求的用户信息拿到,然后我们拿到用户id,将redis中删除就好
@Override
public void loginout() {
/**
* 直接从SeucrityContextHolder中拿到用户信息,找到id,因为这个请求的时候一定会经过我们的过滤链,经过过滤链的时候每次请求的用户信息都会存入到
* SeucrityContextHolder中,我们直接从里面拿到id,然后将redis的用户信息主动删除,这样下次请求再进来时,就是没有登录
*/
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
User user = (User) usernamePasswordAuthenticationToken.getPrincipal();
Long id = user.getId();
redisTemplate.delete(id.toString());
}
6. 实现权限认证
权限认证的思路其实非常简单,我在登录的时候根据用户查询到权限信息,然后交给SpringSecurity框架,并且存到reids中就可以。
1. 第一步就是查询权限信息,那么我们在什么时候查呢,我们之前登录的时候打了todo,在登录的同时就将权限信息查出来然后教给Security
/**
*这个类实现了Sucrity框架的一个接口 UserDetailsService,重写UserDetails方法,
* 将查询出来的User对象存储到UserDetails中(我们自己创建了一个类LoginUserDetails实现了UserDetails接口)
*/
@Service
public class LoginUserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/*
* 这个方法是sucrity框架已经写好的,我们实现UserDetailsService接口重写方法
* 在登录的时候会先走到这一步,我们在这里检验一下用户名是否存在
* */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("username",username);
User user = userMapper.selectOne(userQueryWrapper);
if (ObjectUtils.isEmpty(user)){
throw new RuntimeException("登录失败,用户名或密码输入错误");
}
// TODO: 2023/8/23 查询用户权限信息,在这里我暂时写死的,在实际项目中需要根据RBAC模型联表去查权限信息
ArrayList<String> permissions = new ArrayList<>(Arrays.asList("test","admin"));
user.setPermission(permissions);
// 这里需要返回UserDetails类型,UserDetails是个接口,我们需要自己创建一个类去实现它
// 将user返回之后,sucrity会自己去比较密码是否正确,注意;sucrity比较的时候是将密码加密之后比较,所以在数据库存储时,需要将密码加密
return new LoginUserDetails(user);
}
在这里存到之后,我们在JWT认证过滤器里面,之前也打了todo,需要把权限信息存到SecurityContextHolder中
/**
* 这是我们自定义的Jwt过滤器,来校验token是否正确,保证我们的系统安全
*/
@Component
public class MyJwtFilter extends OncePerRequestFilter {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 首先我们要拿到前端传递过来的token,token是放在请求头中的
String token = request.getHeader("token");
// 这里我们要判定token是否为空,如果为空,就说明没有登录,或者非法请求
if (ObjectUtils.isEmpty(token)) {
// 如果为空我们这里就给放行,不走下面的业务,在Seucrity的框架中后面的过滤器会自己判定
filterChain.doFilter(request, response);
// 记住这里一定要return,因为Security在做完判定在以后,过滤链会再返回走一圈,如果我们这里不return,他就又会走下面的业务代码
return;
}
// 如果token存在,那么我们就解密token并在redis中取出用户信息,存到SeucrutyContext中
try {
Claims claims = JwtUtil.parseJWT(token);
token = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
// 如果这里解析异常报错,就说明token出了问题,我们抛出异常
throw new RuntimeException("token非法");
}
LoginUserDetails loginUserDetails = (LoginUserDetails) redisTemplate.opsForValue().get(token);
if (ObjectUtils.isEmpty(loginUserDetails)) {
// 如果redis中没有存到信息,那么说明要么没登录,要么过期了,我们抛出异常即可
throw new RuntimeException("请登录");
} else {
// TODO: 2023/8/1 在redis中进行续期
}
// TODO: 2023/8/1 下面第三个参数是用户的权限信息,暂时我们没有做权限业务,写为null就可以了
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(loginUserDetails.getUser(), null, loginUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
// 到这一步我们就将用户信息存入到SecurityContextHolder中
// 到最后一步一定记得放行
filterChain.doFilter(request, response);
}
}
然后我们需要在接口方法中添加权限相关注解
@GetMapping
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "hello";
}
好啦,截止到这里简单的Securitu使用就到这里了,如果对您有帮助,请关注加点赞噢。