Spring Security使用教程

环境搭建

  1. 创建一个SpringBoot项目

  2. 引入SpringSecrity依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    

快速入门

认证

  • 自定义自己的实现类,替换SpringSecurity默认的实现类

自定义UserDetailsService

@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private final UserMapper userMapper;

    /**
     *
     * @param s 用户名
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //1.到数据库查询用户信息
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getUserName, s);
        User user = userMapper.selectOne(lambdaQueryWrapper);
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名不存在");
        }
        //2.封装用户信息为UserDetails
        LoginUserDetails loginUserDetails = new LoginUserDetails(user);
        return loginUserDetails;
    }
}

自定义UserDetails

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUserDetails 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;
    }
}

替换默认密码加密和校验实现类

@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    /**
     *修改SpringSecurity的默认密码校验方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 注入一个AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置放行登陆接口,配置自定义认证的过滤器
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf
        http = http.csrf().disable();
        //不通过session获取SecurityContext
        http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
        //匿名访问,未认证可以访问,认证后不可以访问
        http = http.authorizeRequests().antMatchers("/user/login").anonymous()
                //其他接口需要认证才能访问
                .anyRequest().authenticated().and();

        //配置自定义认证过滤器
        http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //认证后才能访问
        //http.authorizeRequests().antMatchers("").authenticated();
        //无论有没有认证都可以访问
        //http.authorizeRequests().antMatchers("").permitAll();
        //没有认证才能访问
        //http.authorizeRequests().antMatchers("").anonymous();
    }
}

自定义登陆接口

  • 控制器
@RequiredArgsConstructor
@RestController
public class LoginController {

    private final LoginService loginService;

    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        return loginService.login(user);
    }

}
  • 服务层
    1. 使用AuthenticationManager进行用户认证
    2. 认证成功,使用用户id生成jwt
    3. 将jwt存入redis
    4. 将jwt返回
@RequiredArgsConstructor
@Service
public class LoginServiceImpl implements LoginService {

    private final AuthenticationManager authenticationManager;
    private final RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        //使用authentication进行用户认证
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //校验是否认证成功
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名或密码错误");
        }
        //使用id生产JWT,JSON Web Token
        LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();
        String token = JwtUtil.createJWT(principal.getUser().getId().toString());
        //将token放到redis中
        redisCache.setCacheObject("user:"+principal.getUser().getId(),principal);
        Map<String,String> map = new HashMap<>();
        map.put("token",token);
        return new ResponseResult<Map<String,String>>(200,"登陆成功",map);
    }
}

修改Security配置,放行自定义登陆接口

  • 常用配置规则

    //认证后才能访问
    http.authorizeRequests().antMatchers("").authenticated();
    //无论有没有认证都可以访问
    http.authorizeRequests().antMatchers("").permitAll();
    //没有认证才能访问
    http.authorizeRequests().antMatchers("").anonymous();
    
  • 添加配置

@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    /**
     *修改SpringSecurity的默认密码校验方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 注入一个AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置放行登陆接口,配置自定义认证的过滤器
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf
        http = http.csrf().disable();
        //不通过session获取SecurityContext
        http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
        //匿名访问,未认证可以访问,认证后不可以访问
        http = http.authorizeRequests().antMatchers("/user/login").anonymous()
                //其他接口需要认证才能访问
                .anyRequest().authenticated().and();

        //配置自定义认证过滤器
        http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //认证后才能访问
        //http.authorizeRequests().antMatchers("").authenticated();
        //无论有没有认证都可以访问
        //http.authorizeRequests().antMatchers("").permitAll();
        //没有认证才能访问
        //http.authorizeRequests().antMatchers("").anonymous();
    }
}

自定义退出登陆接口

  • 控制器
@RequiredArgsConstructor
@RestController
public class LoginController {

    private final LoginService loginService;

    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user){
        return loginService.login(user);
    }

    @RequestMapping("/user/logout")
    public ResponseResult logout(){
        return loginService.logout();
    }
}
	- 服务层
@RequiredArgsConstructor
@Service
public class LoginServiceImpl implements LoginService {

    private final AuthenticationManager authenticationManager;
    private final RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        //使用authentication进行用户认证
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //校验是否认证成功
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名或密码错误");
        }
        //使用id生产JWT,JSON Web Token
        LoginUserDetails principal = (LoginUserDetails) authenticate.getPrincipal();
        String token = JwtUtil.createJWT(principal.getUser().getId().toString());
        //将token放到redis中
        redisCache.setCacheObject("user:"+principal.getUser().getId(),principal);
        Map<String,String> map = new HashMap<>();
        map.put("token",token);
        return new ResponseResult<Map<String,String>>(200,"登陆成功",map);
    }

    @Override
    public ResponseResult logout() {
        //从SecurityContext中获取用户id
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUserDetails userDetails = (LoginUserDetails) authentication.getPrincipal();
        Long id = userDetails.getUser().getId();
        //删除redis中的数据
        redisCache.deleteObject("user:"+id.toString());
        //清除SecurityContext中的数据
        SecurityContextHolder.clearContext();
        return new ResponseResult(200,"退出成功");
    }
}

自定义认证过滤器

  1. 继承OncePerRequestFilter,确保一次请求只经过一次该过滤器
  2. 从请求头中获取token
  3. 解析token获取用户d
  4. 根据用户id从redis中获取用户信息
  5. 将获取到的用户信息封装到UsernamePasswordAuthenticationToken中,存入SecurityContext中
@RequiredArgsConstructor
@Component
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
    private final RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //从请求头中获取token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            //不执行过滤器链后续的操作
            return;
        }

        //解析token获取用户id
        String id;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            id = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis获取用户信息
        LoginUserDetails user = redisCache.getCacheObject("user:" + id);
        if (Objects.isNull(user)) {
            throw new RuntimeException("未登录");
        }
        //将用户信息放入SecurityContext中
        //这里使用三个参数的构造器创建authentication对象,SpringSecurity会讲当前用户设置为已认证状态
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, null);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

在Security过滤器链中添加自定义认证过滤器

  • 在配置类中添加
//配置自定义认证过滤器
http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

授权

给接口添加访问权限限制

  • 在Security配置类中加上注解@EnableGlobalMethodSecurity,启用访问限制
@EnableGlobalMethodSecurity(prePostEnabled = true)
  • 在接口添加访问需要的权限, @PreAuthorize("hasAuthority('test')")
@RestController
public class HelloController {

    @PreAuthorize("hasAuthority('test')")
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}

在用户认证时,查询到用户权限信息,封装到LoginDetails对象中

@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    private final UserMapper userMapper;

    /**
     *
     * @param s 用户名
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //1.到数据库查询用户信息
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getUserName, s);
        User user = userMapper.selectOne(lambdaQueryWrapper);
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名不存在");
        }
        //2.封装用户信息为UserDetails
        //权限信息
        List<String> authorities = Arrays.asList("test","admin");
        LoginUserDetails loginUserDetails = new LoginUserDetails(user,authorities);
        return loginUserDetails;
    }
}
  • UserDetails
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUserDetails implements UserDetails {

    private User user;

    private List<String> permissions;

    public LoginUserDetails(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }
    //忽略序列化
    /**
     * 注意需要将该字段设置为false,否则会报错
     * 且必须有一个属性authorities,否则序列化时会将SimpleGrantedAuthority进行序列化导致反序列化时异常
     */
    @JSONField(serialize = false)
    private transient List<SimpleGrantedAuthority> authorities;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities!=null) {
            return authorities;
        }
        //将权限信息转换为GrantedAuthority
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @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;
    }
}

在认证过滤器中查询到用户权限信息保存到SecurityContext中保存

  • 关键代码
//将用户信息和用户权限封装到authentication
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
  • 完整代码
@RequiredArgsConstructor
@Component
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
    private final RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //从请求头中获取token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            //不执行过滤器链后续的操作
            return;
        }

        //解析token获取用户id
        String id;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            id = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis获取用户信息
        LoginUserDetails user = redisCache.getCacheObject("user:" + id);
        if (Objects.isNull(user)) {
            throw new RuntimeException("未登录");
        }
        //将用户信息放入SecurityContext中
        //这里使用三个参数的构造器创建authentication对象,SpringSecurity会讲当前用户设置为已认证状态
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

自定义异常处理

  • 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到
  • 在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常
  • 如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可

自定义认证异常处理

  • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //创建响应对象
        ResponseResult<String> result = new ResponseResult<>(HttpStatus.UNAUTHORIZED.value(),"认证失败"+ e.getMessage());
        //转换为JSON字符串
        String json = JSON.toJSONString(result);
        //封装响应
        WebUtils.renderString(httpServletResponse,json);
    }
}

自定义授权异常处理

  • 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        //创建响应对象
        ResponseResult<String> result = new ResponseResult<>(HttpStatus.UNAUTHORIZED.value(),"权限不足"+ e.getMessage());
        //转换为JSON字符串
        String json = JSON.toJSONString(result);
        //封装响应
        WebUtils.renderString(httpServletResponse,json);
    }
}

配置自定义异常处理器

  • 核心部分
//配置自定义认证异常处理器
http = http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and();
//配置自定义授权异常处理器
http = http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and();
  • 完整代码
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final AccessDeniedHandler accessDeniedHandler;
    /**
     *修改SpringSecurity的默认密码校验方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 注入一个AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置放行登陆接口,配置自定义认证的过滤器
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf
        http = http.csrf().disable();
        //不通过session获取SecurityContext
        http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
        //匿名访问,未认证可以访问,认证后不可以访问
        http = http.authorizeRequests().antMatchers("/user/login").anonymous()
                //其他接口需要认证才能访问
                .anyRequest().authenticated().and();

        //配置自定义认证过滤器
        http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //认证后才能访问
        //http.authorizeRequests().antMatchers("").authenticated();
        //无论有没有认证都可以访问
        //http.authorizeRequests().antMatchers("").permitAll();
        //没有认证才能访问
        //http.authorizeRequests().antMatchers("").anonymous();


        //配置自定义异常处理器

        //配置自定义认证异常处理器
        http = http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and();
        //配置自定义授权异常处理器
        http = http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and();
    }
}

解决SpringSecurity中的跨域问题

在SpringBoot中允许跨域访问

  • 通过是实现WebMvcConfigurer的方式配置允许跨域
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                //允许跨域的url
                .addMapping("/**")
                //允许所有源访问,允许跨域
                .allowedOriginPatterns("*")
                //允许使用cookie
                .allowCredentials(true)
                //允许的请求方式
                .allowedMethods("GET","POST","PUT","DELETE")
                //允许设置请求头
                .allowedHeaders("*")
                //允许跨域的时间
                .maxAge(3600);
        
    }
}

在SpringSecurity中允许跨域访问

  • 核心代码
//配置允许跨域
http.cors();
  • 完整代码
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final AccessDeniedHandler accessDeniedHandler;
    /**
     *修改SpringSecurity的默认密码校验方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 注入一个AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置放行登陆接口,配置自定义认证的过滤器
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf
        http = http.csrf().disable();
        //不通过session获取SecurityContext
        http = http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
        //匿名访问,未认证可以访问,认证后不可以访问
        http = http.authorizeRequests().antMatchers("/user/login").anonymous()
                //其他接口需要认证才能访问
                .anyRequest().authenticated().and();

        //配置自定义认证过滤器
        http = http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //认证后才能访问
        //http.authorizeRequests().antMatchers("").authenticated();
        //无论有没有认证都可以访问
        //http.authorizeRequests().antMatchers("").permitAll();
        //没有认证才能访问
        //http.authorizeRequests().antMatchers("").anonymous();


        //配置自定义异常处理器

        //配置自定义认证异常处理器
        http = http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and();
        //配置自定义授权异常处理器
        http = http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and();

        //配置允许跨域
        http.cors();
    }
}

自定义权限校验方法

  • 使用SPEL表达式获取spring容器中bean,然后调用bean对应的方法进行校验
  • 使用@beanName的方式可以获取spring容器中的bean
  • 使用#{beanName.age}的方式可以在xml配置文件中使用spring容器bean的属性和方法

自定义权限校验

  • 自定义权限校验逻辑,并注入spring容器
@Component("myAuthentication")
public class MyAuthenticationHandler {
    public boolean hasAuthority(String authority){
        //获取用户具有的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUserDetails user = (LoginUserDetails) authentication.getPrincipal();
        List<String> permissions = user.getPermissions();
        //判断用户是否具有访问的权限
        return permissions.contains(authority);
    }
}

在接口使用自定义的权限校验方法

  • 使用SPEL表达式的@方式获取spring容器中的bean,调用bean的方式进行权限校验
  • @myAuthentication.hasAuthority('system:dept:list')表示获取spring容器中名称为myAuthentication的bean,然后调用hasAuthority方法进行权限校验
  • 校验流程与SpringSecurity默认流程类型,只需要校验方法返回一个布尔值即可
@RestController
public class HelloController {

    @PreAuthorize("hasAuthority('system:dept:list')")
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }

    /**
     * 使用自定义权限校验方法
     * @return
     */
    @PreAuthorize("@myAuthentication.hasAuthority('system:dept:list')")
    @RequestMapping("/testMyAuthenticationHandler")    
    public ResponseResult testMyAuthenticationHandler(){
        return new ResponseResult(HttpStatus.OK.value(),"校验通过");
    }
}