SpringSecurity使用详细讲解,附源码详细每一步的引导

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使用就到这里了,如果对您有帮助,请关注加点赞噢。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Spring框架是一个开的Java应用程序框架,用于构建企业级应用程序。它提供了一种轻量级的、非侵入式的方式来管理Java对象的生命周期和依赖关系,从而简化了应用程序的开发和维护过程。下面是对Spring框架码的简要讲解: 1. 核心容器(Core Container):Spring框架的核心是其IoC容器(Inversion of Control)。它通过IoC容器来管理和组织应用程序中的Bean对象。在IoC容器中,Spring使用`BeanFactory`接口和其实现类`DefaultListableBeanFactory`来加载、配置和管理Bean对象。 2. 上下文(Context):Spring还提供了更高级的上下文功能,如`ApplicationContext`接口和其实现类`AnnotationConfigApplicationContext`、`XmlWebApplicationContext`等。上下文扩展了`BeanFactory`的功能,提供了更多的企业级特性,如国际化、事件传播、资加载等。 3. AOP(Aspect-Oriented Programming):Spring框架实现了AOP的支持,用于将横切关注点(cross-cutting concerns)从核心业务逻辑中分离出来。Spring AOP基于代理模式和动态代理技术,通过面向切面编程来实现横切关注点的模块化。 4. 数据访问(Data Access):Spring框架提供了对各种数据访问技术的支持,包括JDBC、ORM(如Hibernate、MyBatis)和事务管理。它通过抽象出数据访问层的公共接口,使得应用程序可以更容易地切换和集成不同的数据访问技术。 5. Web支持:Spring框架还提供了对Web应用程序开发的支持,包括Web MVC、RESTful服务、WebSocket等。它通过`DispatcherServlet`和一系列的处理器(Handler)来处理Web请求,并提供了一套强大的机制来实现灵活的Web应用程序开发。 6. 测试支持:Spring框架还提供了对单元测试和集成测试的支持,如`JUnit`和`TestNG`的集成、模拟对象(Mock Objects)的支持等。这使得开发人员可以更方便地编写和执行各种测试用例。 需要注意的是,Spring框架是一个非常庞大而复杂的项目,其代码具有很高的复杂性和技术难度。如果您有兴趣深入了解Spring框架的码,建议您从官方网站下载代码并阅读相关文档,或者参考一些专门讲解Spring码的书籍和教程。同时,熟悉Java编程、设计模式和IoC、AOP等基本概念也是理解和阅读Spring码的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值