蔚然丶丶 2023-09-13 16:12 采纳率: 0%
浏览 34

SpringSecurity自定义多种登录方式,登录成功之后没有认证信息,是匿名角色

SpringSecurity多种登录方式,模拟手机验证码登录,实现Security中的Filter、Provider、Token,结果可以登录成功,但不能进行权限控制,获取Security上下文显示是匿名角色ROLE_ANONYMOUS,请问下面代码哪里理解有问题?应该如何修改?

SpringBoot3+SpringSecurity6+ajax

查看SpringSecurity文档,是不是和这个有关系?
https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html#requireexplicitsave
https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html#customizing-where-authentication-is-stored

参考
https://stackoverflow.com/questions/64679097/spring-security-returning-anonymous-user-after-successful-authenticaiton
https://stackoverflow.com/questions/74341976/spring-security-an-authentication-object-was-not-found-in-the-securitycontext

Filter

/**
 * Filter负责拦截请求并调用Manager
 * Manager负责管理多个Provider,并选择合适的Provider进行认证
 * Provider负责认证,检查账号密码之类的
 * Token是认证信息,包含账号密码,具体可以自己定义
 */
public class MyMultipleLoginFilter extends AbstractAuthenticationProcessingFilter {

    private AuthenticationManager authenticationManager;

    // 要拦截的登录请求
    public MyMultipleLoginFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
    public MyMultipleLoginFilter(AuthenticationManager authenticationManager){
        super(new AntPathRequestMatcher("/login", "POST"));
        this.authenticationManager = authenticationManager;
    }


    /**
     * 封装信息,提交认证
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

        if("GET".equals(request.getMethod())){
            throw new AuthenticationServiceException("MyMultipleLoginFilter method not supported: " + request.getMethod());
        }else{
            String phone = request.getParameter("phone");
            String code = request.getParameter("code");
            System.out.println("phone:" + phone + " code:" + code);
            // 自定义token
            MyMultipleAuthenticationToken token = new MyMultipleAuthenticationToken(phone, code);

            return this.getAuthenticationManager().authenticate(token);
        }

    }

    @Override
    protected AuthenticationSuccessHandler getSuccessHandler() {
        return super.getSuccessHandler();
    }
}

Provider

/**
 * 用于验证自定义的服务是否与预期一致,如用户输入的邮箱验证码和预期的验证码
 *
 *  Filter负责拦截请求并调用Manager
 *  Manager负责管理多个Provider,并选择合适的Provider进行认证
 *  Provider负责认证,检查账号密码之类的
 *  Token是认证信息,包含账号密码,具体可以自己定义
 */
@Component
public class MyMultipleAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    SecurityUserDetailsServiceImpl userDetailsService;

    /**
     * 验证信息
     * @param authentication 封装的验证信息?token?
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        // 认证代码

        // 转为自定义token,里面封装了认证时需要的数据
        MyMultipleAuthenticationToken myAuthToken = (MyMultipleAuthenticationToken) authentication;


        // 获取登录phone
        String phone = (String) myAuthToken.getPrincipal();
        // 获取用户输入凭证,如邮箱、手机验证码
        String code = (String) myAuthToken.getCredentials();

        System.out.println("手机验证:phone=" + phone + " code=" + code);
        if(phone.isEmpty() || code.isEmpty()){
            return null;
        }

        // 查询数据库获取用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(phone);


        // 去数据库/redis查询验证码是否正确,然后判断抛出异常等情况
        // 此处简写从数据库/redis读取验证码,仍以账号密码代替手机号和验证码,给明文密码加密以对比数据库中的密码
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        if(!passwordEncoder.matches(code, userDetails.getPassword())){
            // throw new BadCredentialsException("验证码不正确");
            throw new RuntimeException("验证码不正确");
        }

        // 返回认证后的token
        MyMultipleAuthenticationToken authenticationToken = new MyMultipleAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
        authenticationToken.setDetails(userDetails);
        
        return authenticationToken;

    }

    // 判断token支持类,验证传入的身份验证对象是否是指定的 xxToken
    @Override
    public boolean supports(Class<?> authentication) {
        return MyMultipleAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

Token

/**
 * 封装验证信息,根据自定义验证类型,如封装邮箱验证码信息
 * Token的核心在于两个构造方法,一个是认证前使用,一个是认证后使用。
 *
 *  Filter负责拦截请求并调用Manager
 *  Manager负责管理多个Provider,并选择合适的Provider进行认证
 *  Provider负责认证,检查账号密码之类的
 *  Token是认证信息,包含账号密码,具体可以自己定义
 */
public class MyMultipleAuthenticationToken extends AbstractAuthenticationToken {

    // 如邮箱、手机号
    private Object principal;
    // 如邮箱验证码、手机验证码
    private Object credentials;


    /**
     * 认证时过滤器通过这个方法创建Token,传入前端的参数
     * @param credentials phone
     * @param principal code
     */
    public MyMultipleAuthenticationToken(Object principal, Object credentials){
        super(null);
        this.credentials = credentials;
        this.principal = principal;
        //关键:标记未认证
        setAuthenticated(false);
    }

    /**
     * 认证通过后Provider通过这个方法创建Token,传入自定义信息以及授权信息
     * @param credentials phone
     * @param principal code
     * @param authorities List auth
     */
    public MyMultipleAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.credentials = credentials;
        this.principal = principal;
        //关键:标记已认证
        super.setAuthenticated(true);

    }


    public static MyMultipleAuthenticationToken authenticated(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        return new MyMultipleAuthenticationToken(principal, credentials, authorities);
    }

    @Override
    public Object getCredentials() {
        return credentials;
    }

    @Override
    public Object getPrincipal() {
        return principal;
    }
}

Config

/**
 * Security配置类
 */
@EnableWebSecurity
@Configuration
public class SecurityConfig {


    @Autowired
    SecurityUserDetailsServiceImpl userDetailsService;

    // 自定义验证
    @Autowired
    MyMultipleAuthenticationProvider myMultipleAuthenticationProvider;

    @Bean
    public AuthenticationManager authenticationManager() {
        return new ProviderManager(Arrays.asList(myMultipleAuthenticationProvider));
    }

    @Bean
    public MyMultipleLoginFilter myMultipleLoginFilter(){
        MyMultipleLoginFilter myMultipleLoginFilter = new MyMultipleLoginFilter();
        myMultipleLoginFilter.setAuthenticationManager(authenticationManager());
        myMultipleLoginFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                // response.getWriter().write("success!");

                System.out.println("成功之后将auth放入上下文");
                SecurityContextHolder.getContext().setAuthentication(authentication);

                response.setContentType("text/json;charset=utf-8");
                Result result = new Result();
                result.setCode(1);
                result.setStatus(222);
                result.setMsg("登录成功!");
                ObjectMapper mapper = new ObjectMapper();
                ServletOutputStream outputStream = response.getOutputStream();
                mapper.writeValue(outputStream, result);
                // response.sendRedirect("/home");
            }
        });
        return myMultipleLoginFilter;
    }



    /**
     * 密码编码器
     * @return
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {


        return http

                .authorizeHttpRequests(auth->{

                    // 设置url权限,注意所有权限的配置顺序
                    auth.requestMatchers("/home").permitAll();
                    auth.requestMatchers("/login").permitAll();
                    auth.requestMatchers("/phone_login").permitAll();
                    auth.requestMatchers("/login_phone").permitAll();
                    auth.requestMatchers("/test").permitAll();

                    // 验证码
                    auth.requestMatchers("/captcha/**").permitAll();
                    // 静态资源
                    auth.requestMatchers("/js/**").permitAll();
                    auth.requestMatchers("/home/l0").hasRole("USER");
                    auth.requestMatchers("/home/l1/**").hasRole("Dog");
                    auth.requestMatchers("/home/l2/**").hasRole("Cat");
                    auth.anyRequest().authenticated();
                    // auth.anyRequest().permitAll();
                })
                // 禁用session
                .sessionManagement(session->{
                    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                })
                .userDetailsService(userDetailsService)
                .authenticationProvider(myMultipleAuthenticationProvider)
                .addFilterBefore(myMultipleLoginFilter(), UsernamePasswordAuthenticationFilter.class)

                .csrf(conf -> {conf.disable();})// 关闭跨站请求伪造保护功能
                .build();
    }
}

登录响应

http://localhost:8080/login
{"code":1,"status":222,"msg":"登录成功!"}

打印上下文

{
    "authorities": [
        {
            "authority": "ROLE_ANONYMOUS"
        }
    ],
    "details": {
        "remoteAddress": "0:0:0:0:0:0:0:1",
        "sessionId": null
    },
    "authenticated": true,
    "principal": "anonymousUser",
    "keyHash": -1412005665,
    "credentials": "",
    "name": "anonymousUser"
}

控制台数据登录时的认证,是有的

MyMultipleAuthenticationToken [Principal=$2a$10$5PHUgYh8A31715Vb0pjR3OWPiF8Y/Ns1P5BaSqGtD/YMC/EqyjCYa, Credentials=[PROTECTED], Authenticated=true, Details=org.springframework.security.core.userdetails.User [Username=pop, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_Cat]], Granted Authorities=[ROLE_Cat]]

在网上查找信息,有一篇文章提到是因为Security在认证后会把上下文清空,所以需要再添加一个过滤器手动将认证信息放入上下文中,但是不能在这个过滤器中获取登录信息
权限管理03-security登陆后鉴权_security登录验证与鉴权_高秉文的博客-CSDN博客

/**
 * 解决天坑坑坑坑坑----------------
 * 登录成功,一直401,显示匿名用户鉴权失败,是因为SecurityContextHolder自己把Context清楚了,我们需要重新设置一下
 *
 * @author gaorimao
 * @date 2022/02/10
*/
@Component
public class MyOncePerRequestFilter extends OncePerRequestFilter {


    @Autowired
    SecurityUserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
        if(!"/login".equals(request.getRequestURI())){

            // 获取登录phone,不能读取,只能先写死测试
            // String phone = request.getParameter("phone");
            // 获取用户输入凭证,如邮箱、手机验证码
            // String code = request.getParameter("code");
            
            String phone = "pop";
            String code = "1";

            System.out.println("手机验证:phone=" + phone + " code=" + code);
            if(phone.isEmpty() || code.isEmpty()){

            }

            // 数据库校验用户名密码
            UserDetails userDetails = userDetailsService.loadUserByUsername(phone);

            /*
                重新设置SecurityContextHolder
             */

            MyMultipleAuthenticationToken myMultipleAuthenticationToken = new MyMultipleAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
            myMultipleAuthenticationToken.setDetails(userDetails);

            SecurityContextHolder.getContext().setAuthentication(myMultipleAuthenticationToken);
        }
        filterChain.doFilter(request, response);
    }


}
  • 写回答

1条回答 默认 最新

  • 专家-赤兔[在线] 全栈领域优质创作者 2023-09-14 10:07
    关注

    引用 皆我百晓生 小程序回复内容作答:

    根据你提供的代码,我注意到以下几个问题可能导致登录成功后无法进行权限控制,获取的Security上下文显示为匿名角色:

    1.在 MyMultipleLoginFilter 的构造方法中,你没有将 authenticationManager 赋值给类的成员变量 authenticationManager,只是简单地调用了父类的构造方法。因此,你需要在构造方法中将参数 authenticationManager 赋值给 MyMultipleLoginFilter 类的成员变量:

    public MyMultipleLoginFilter(AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher("/login", "POST"));
        this.authenticationManager = authenticationManager;
    }
    

    2.在 MyMultipleLoginFilter 类中,attemptAuthentication 方法中使用了 getAuthenticationManager() 方法获取 AuthenticationManager 进行认证,但是在该类的构造方法中并没有将 authenticationManager 赋值给属性 this.authenticationManager。所以你需要在构造方法中将传入的 authenticationManager 赋值给该属性:

    public MyMultipleLoginFilter(AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher("/login", "POST"));
        this.authenticationManager = authenticationManager;
    }
    

    3.在 MyMultipleAuthenticationProviderauthenticate 方法中,你使用 MyMultipleAuthenticationTokengetPrincipal() 方法获取了用户输入的验证码,但这是不正确的。正确的是,应该使用 MyMultipleAuthenticationTokengetCredentials() 方法获取用户输入的验证码:

    String code = (String) myAuthToken.getCredentials();
    

    4.在 MyMultipleAuthenticationProviderauthenticate 方法中,你通过 BCryptPasswordEncodermatches 方法将用户输入的验证码与数据库中的密码进行比较。然而,应该使用 BCryptPasswordEncodermatches 方法将用户输入的验证码与数据库中的验证码进行比较:

    if(!passwordEncoder.matches(code, userDetails.getPassword())) {
        throw new RuntimeException("验证码不正确");
    }
    

    5.在 MyMultipleAuthenticationProviderauthenticate 方法中,你应该返回经过认证后的 authenticationToken,而不是设置另一个 authenticationToken 对象并将其放入 SecurityContextHolder 中。因为在 MyMultipleLoginFilter 中已经进行了认证,所以不需要在这里再次进行认证:

    return authenticationToken;
    

    6.在 SecurityConfig 中的 configure 方法中,你设置了身份验证过滤器 myMultipleLoginFilter 并指定了成功处理程序,但是没有指定身份验证失败的处理程序。所以你可以通过调用 myMultipleLoginFiltersetAuthenticationFailureHandler 方法来指定身份验证失败的处理程序:

    myMultipleLoginFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"));
    

    通过修改上述问题中提到的地方,应该能够解决登录成功后没有认证信息的问题,并且可以正确进行权限控制。请尝试修改你的代码,并测试是否能够获得正确的认证信息。如果还有其他问题,请提供更多详细的信息以便我更好地帮助你。

    评论 编辑记录

报告相同问题?

问题事件

  • 修改了问题 9月20日
  • 修改了问题 9月14日
  • 修改了问题 9月14日
  • 修改了问题 9月14日
  • 展开全部

悬赏问题

  • ¥30 模拟电路 logisim
  • ¥15 PVE8.2.7无法成功使用a5000的vGPU,什么原因
  • ¥15 is not in the mmseg::model registry。报错,模型注册表找不到自定义模块。
  • ¥15 安装quartus II18.1时弹出此error,怎么解决?
  • ¥15 keil官网下载psn序列号在哪
  • ¥15 想用adb命令做一个通话软件,播放录音
  • ¥30 Pytorch深度学习服务器跑不通问题解决?
  • ¥15 部分客户订单定位有误的问题
  • ¥15 如何在maya程序中利用python编写领子和褶裥的模型的方法
  • ¥15 Bug traq 数据包 大概什么价