SpringBoot集成Spring Security

本文介绍了SpringBoot集成Spring Security的实现过程,包括AuthenticationManager、AuthenticationProvider、UserDetails等核心组件的工作原理,以及配置、登录验证流程。通过示例代码展示了如何配置Spring Security,并提供了JWT验证的扩展思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在学习springboot 及微服务框架时,在搭建项目的时候想实现登录注册的功能,在网上翻翻找找,感觉还是现在目前比较流行的Spring Security比较靠谱,所以自己尝试搭建了下。spring Security的特性,如果感兴趣可以查一查,毕竟我不是它的推广者么,只是使用者,如果你感觉它还可以,那就有必要到这里来看下了,这里只讲一下其简单的工作原理和搭建流程。

说明

在开始之前,个人首先说明下,目前使用spring Security 目前主要的2个场景, 一个是单个服务的使用session的场景,另一个是使用JWT验证的 微服务模式。在这里本人做的是基于session的验证,但是在后面会留出 JWT验证的扩展。

源码和原理----简单分析

说到Security, 我们首先会想到就是 用户的登录验证。所以先从验证这一点慢慢来进行剖析其原理。

AuthenticationManager

首先,需要讲明的是AuthenticationManager,其是认证管理器,通过源码可知他是一个接口,只对认证做了一个声明验证的方法。具体的验证方法,都交由Provider 来实现。

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

AuthenticationProvider

AuthenticationProvider 也是一个接口,他是最基本的接口,也定义了验证的方法,所以我们具体实现的时候只要定义一个自己的类,实现这个接口,复写一下认证方法就行了。

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;


public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);
}

PS: 有人可能问,AuthenticationManager 和 AuthenticationProvider 都有认证,而且都是接口,怎么交付给Provider来验证的?源码中个人没看到,但是我试着猜想下,可能是Manager的实现类中验证方法调用了provider的 啊!

UserDetails 

好吗,其实当个人创建实现AuthenticationProvider类,进行验证,其实到这就完成基本功能的一半多了。现在想一下,自己的实现类如何验证?肯定是从前段获取的数据 与 数据库存储的信息进行对比么。现在基本的开发框架中 持久层中,基本上都是ORM的了吧。 Security其实也为我们提供了一个用户的基本的接口 UserDetails。其对特定的方法做了定义:

package org.springframework.security.core.userdetails;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.io.Serializable;
import java.util.Collection;


public interface UserDetails extends Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();

	String getUsername();

    //下面的几个参数,怎么用自己设计

    //账号是否过期
	boolean isAccountNonExpired();
	//账号是否被锁
	boolean isAccountNonLocked();
	//身份是否过期
	boolean isCredentialsNonExpired();
    //是否可用
	boolean isEnabled();
}

UserDetailsService

有了实体类了,那就需要操纵这个实体类的 service 层了,UserDetailsService这个接口就是对默认接口的定义,目前 只是定义了一个函数,这个函数是其验证默认调用的,自己验证的时候可以用别的函数,但是 为了原生态点还是不要动了。

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

这就相当于 业务逻辑层,当然mapper 数据持久层害的自己去实现,无非是用户的增删改查功能。这些全都弄完了,就差一个配置问题了,你不把 Provider 配置到Security中去,他还是会默认自己原来的配置。

这个配置的基本接口是 WebSecurityConfigurer,但是现在已经有WebSecurityConfigurerAdapter抽象类对其 相应的方法进行了实现和扩展,所以我们只需要集成这个类,重写一下 配置方法就可以了。

到现在基本的验证 基本就可以了。下面实现的时候再加入handler的和filter的。

流程

现在抛出上面的想法,我们以一个完整的流程说一下工作流程。

  1. 前段输入用户名密码,点击登录按钮,跳转到security配置中配置的登录验证url    (这个url可以配置)
  2. 相应的filter 对 请求进行拦截处理(这个filter 也可以自己定义,使用JWT时需要自己实现)
  3. 交付AuthenticationManager,并交由provider进行验证(验证过程中,需要我们实现的实体类,service层和mapper 持久层,这些都是我们自己定义实现的)。
  4. 将验证结果返回相应的filter
  5. filter在调用处理相应 状况的handler 进行最后处理(这个也可以自己去实现)

实现

  1.  添加依赖 
     <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <version>2.0.0.RELEASE</version>
            </dependency>

    添加完依赖后,启动项目访问url后会出现一个登录的页面,用户名是 user, 密码在后台log中打印出来

  2.  配置springSecurity 的配置
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        /**
         * @Authortao
         * @Description  覆写父类的request 配置,主要是对请求路径和资源的限制
         * @Param
         * @return
         * @Date 19:34 2018-12-27
         **/
        @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http
                .authorizeRequests()
                    .antMatchers("/register").permitAll()  //允许所有人 调用注册请求
                    .anyRequest().authenticated() //其他请求必须要验证
                .and()
                    //设置免登陆的页面和资源
                .formLogin()
                    .loginProcessingUrl("/login/submit")    //设置登录验证的url
                    .permitAll()
                .and()
                    .csrf().disable();//关闭csrf验证
            super.configure(http);
        }

     

  3.  添加自己创建的provider 并实现认证方法
    /**
     * @ClassName UserAuthenticationProvider
     * @Descriptiom 我们自定义的provider 对象
     * @Author 
     * @Date 2018-12-27 20:37
     * @Version 1.0v
     **/
    
    @Component
    public class UserAuthenticationProvider implements AuthenticationProvider {
        @Resource
        private UserService userDetailService;
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        //开始验证
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            System.out.println("provider 验证开始");
            //获取用户名和密码
            String username = authentication.getName();
            String password = (String) authentication.getCredentials();
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            //查看是否含有该用户信息
            User user = (User) userDetailService.loadUserByUsername(username);
    
            //如果没有
            if (user == null){
                throw  new BadCredentialsException("用户不存在!!");
            }
    
            if( ! encoder.matches(password,user.getPassword())){
    
                throw  new BadCredentialsException("密码不正确!!");
            }
    
            return new UsernamePasswordAuthenticationToken(username,password,authorities);
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            return true;
        }
    }

    修改 配置文件,并添加一下代码,重启项目

      @Resource
        UserAuthenticationProvider provider; 
     /**
         * @Author
         * @Description 覆盖父类的关于 验证的配置
         * @Param
         * @return
         * @Date 19:34 2018-12-27
         **/
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //配置验证的provider
            auth.authenticationProvider(provider);
        }

    打个断点启动项目,你会发现配置么,就是在启动的时候编译的。

  4.  美化验证结果输出,实现相应的Handler
    @Component("userAuthenticationSuccessHandler")
    public class UserAuthenticationSuccessHandler  extends SavedRequestAwareAuthenticationSuccessHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
            System.out.println("这是provider的成功验证");
    //        super.onAuthenticationSuccess(request, response, authentication);
            Map<String,String> map=new HashMap<>();
            map.put("code", "2000");
            map.put("msg", "登录成功");
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(map));
    
    
            //如果是要跳转到某个页面的
            new DefaultRedirectStrategy().sendRedirect(request, response, "/dudu/sss");
        }
    }
    
    @Component("userAuthenticationFailHander")
    public class UserAuthenticationFailHander extends SimpleUrlAuthenticationFailureHandler {
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            System.out.println("这是Hander的失败验证");
    
    //        super.onAuthenticationFailure(request, response, exception);
            Map<String,String> map=new HashMap<>();
            map.put("code", "2001");
            map.put("msg", "登录失败");
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(map));
        }
    }

    并修改 配置类为

     protected void configure(HttpSecurity http) throws Exception {
    
            http
                .authorizeRequests()
                    .antMatchers("/register").permitAll()
                    .anyRequest().authenticated()
                .and()
                    //设置免登陆的页面和资源
                .formLogin()
                    .loginProcessingUrl("/login/submit")
                    .failureHandler(userAuthenticationFailHander)
                    .successHandler(userAuthenticationSuccessHandler)
                    .permitAll()
                .and()
                    .addFilter(new UserAuthenticationFilter(authenticationManager(),userAuthenticationFailHander,userAuthenticationSuccessHandler))
            super.configure(http);
        }

     

  5.  倘若想 实现Filter
    public class UserAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    
        private AuthenticationManager authenticationManager;
    
        UserAuthenticationFailHander userAuthenticationFailHander;
    
        @Autowired
        UserAuthenticationSuccessHandler userAuthenticationSuccessHandler;
    
        private RememberMeServices rememberMeServices = new NullRememberMeServices();
    
        public UserAuthenticationFilter(AuthenticationManager authenticationManager,
                                        UserAuthenticationFailHander userAuthenticationFailHander, UserAuthenticationSuccessHandler userAuthenticationSuccessHandler) {
    
            //这个构造方法实在启动、编译的时候调用,
            this.authenticationManager = authenticationManager;
            this.userAuthenticationFailHander = userAuthenticationFailHander;
            this. userAuthenticationSuccessHandler =  userAuthenticationSuccessHandler;
            //添加拦截的路径
            super.setFilterProcessesUrl("/login/submit");
        }
    
        //当相应的路径进行请求的时候,进行向相应的处理
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            System.out.println("我们要执行filter 了哈");
    
            //这个方法说实话现在不能用,因为这个是form 提交啊, 没有指定 相应元素的 name 元素
    //            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
    
            String username = obtainUsername(request);
            String password = obtainPassword(request);
            {
                //这里可以做一下登录的验证啊,比如: 用户名、密码是否为空,验证码的正确与否
            }
    
    
            //倘若上述的验证全都通过的话,我们去验证用户名和密码是否匹配, 下面是存储的权限
            Collection<? extends GrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN"));
            //验证的时候,他的基本原理是 AuthenticationManager进行验证,但是其是一个接口,具体的实现在相应的provider中
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(username, password, authorities));
    //        return super.attemptAuthentication(request, response);
    
        }
    
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
            System.out.println("大家注意了哈!!!!!,这是 filter的 成功验证调用,successfulAuthentication-------------------------------------->>>>>>>>>>>>>>>>>>");
            SecurityContextHolder.getContext().setAuthentication(authResult);
            userAuthenticationSuccessHandler.onAuthenticationSuccess(request, response, authResult);
    //        rememberMeServices.loginSuccess(request, response, authResult);
    //        super.successfulAuthentication(request, response, chain, authResult);
        }
    
        @Override
        protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
            System.out.println("大家注意了哈!!!!!,这是 filter的 失败验证调用,unsuccessfulAuthentication****************************************************************");
            SecurityContextHolder.clearContext();
    
            if (logger.isDebugEnabled()) {
                logger.debug("Authentication request failed: " + failed.toString(), failed);
                logger.debug("Updated SecurityContextHolder to contain null Authentication");
                logger.debug("Delegating to authentication failure handler " + userAuthenticationFailHander);
            }
    
    //        rememberMeServices.loginFail(request, response);
    
            userAuthenticationFailHander.onAuthenticationFailure(request, response, failed);
    //        super.unsuccessfulAuthentication(request, response, failed);
        }
    }

    这里需要再次修改配置类

       @Autowired
        UserAuthenticationFailHander userAuthenticationFailHander;
    
        @Autowired
        UserAuthenticationSuccessHandler userAuthenticationSuccessHandler;
    @Override
        protected void configure(HttpSecurity http) throws Exception {
    
            http
                .authorizeRequests()
                    .antMatchers("/register").permitAll()
                    .anyRequest().authenticated()
                .and()
                    //设置免登陆的页面和资源
                .formLogin()
                    .loginProcessingUrl("/login/submit")
    //                .failureHandler(userAuthenticationFailHander)
    //                .successHandler(userAuthenticationSuccessHandler)
                    .permitAll()
                .and()
                    .addFilter(new UserAuthenticationFilter(authenticationManager(),userAuthenticationFailHander,userAuthenticationSuccessHandler))
    //                // 不需要session
    //                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    //            .and()
                    .csrf().disable();
            super.configure(http);
        }

     

  6. 如果想使用JWT,则需要在filter 验证成功后,创建一个token,并存入 Redis中, 并将token塞入header 中返回给前段,让前段以此登录认证。另外还需要将上面 session 关闭,这时候就不要session 存储登录信息了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值