springboot 集成Security 框架(1)

本文详细介绍了如何在SpringBoot项目中集成Security框架,包括基本配置、表单登录设置、自定义用户验证和密码策略。重点讲解了如何实现自定义登录接口、密码校验以及密码编码器的选择。

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

springboot 集成Security 框架

之前的项目都是用的Security 作为鉴权框架,项目中实际也用了四五个了,可是发现每次用的时候都记不住了,所以还是记下来吧,作为自己的笔记吧

springboot 先引入Security依赖,Security 已经有了boot 的starter 包直接可以引入

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- security 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

什么都不用配置,启动项目,这个时候security已经起作用了。随便访问项目的一个地址,可以看到都被定向到了登陆页面。

在这里插入图片描述

账户user ,密码是控制打印的,确保日志打印级别为info就能看到。

现在只有一个固定的user用户,密码每次都是启动时随即生成的,真正的项目肯定无法使用的,现在来对security 进行配置。

新建一个配置类SecurityConfig 继承于 WebSecurityConfigurerAdapter


package z.c.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;


/**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
/**
 * Security 配置
 * @author zhoupan
 * @data 2021/8/11 16:19
 * @description
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
//        String redirectUrl = securityProperties.getBrowser().getLoginPage();
        //basic 登录方式
//      http.httpBasic()


        http
                //开启表单登录方式
                .formLogin()
                //自定义的登陆页跳转地址,不设置默认为/login,设置后需要将设置地址设置为任何人可以访问,不然会重定向循环
                .loginPage("/login")
                //登录提交的请求,不设置时和loginPage设置的地址一样,单独设置没有意义,还发现一个挺奇怪的点,当配置了successHandler时,如果没有在successHandler处理器中跳转其他地址,那么浏览器就会跳转到loginProcessingUrl配置的地址,这是个虚地址, 你可以没有该地址
                .loginProcessingUrl("/login/form")
                // 认证成功的转发地址 ,如果没有设置则按照跳转到登陆前的地址。即会记忆地址,登陆后又到原地址。这里有
                //个坑如果这个地址最终返回的是个视图则会报错 ,因为登陆是post请求,而mvc不允许post请求返回视图 ,只要要改为转发就行 。        
                .successForwardUrl("/test/a")
                //默认的认证成功跳转路径,这里和successForwardUrl的区别是,如果加上参数true效果是一样的,如果不加,则没有记忆地址时会跳到该地址。
                //successForwardUrl和defaultSuccessUrl两个都设置则以defaultSuccessUrl为准
                .defaultSuccessUrl("/test/b",true)
                //登录成功处理器,设置了登录认证成功就会执行,在这里可以直接返回,配置了这个处理器后,
                //loginProcessingUrl和defaultSuccessUrl就不起作用了
//                .successHandler(mySuccessHandler)
                //登录失败处理器
//                .failureHandler(myFailHandler)
                .and()
                //请求授权
                .authorizeRequests()
                //不需要权限认证的url
                .antMatchers("/login","/login/form").permitAll()
                //其他的任何请求 需要身份认证
                .anyRequest().authenticated()
                .and()
                //关闭跨站请求防护
                .csrf().disable();
    }
}



代码中有详细的注释,可以看到开启了表单登录后,可以对登录地址,登录请求地址,登录成功跳转地址以及失败成功处理器的设置(当然还有很多其他的配置这里没有写)
基于这个配置先实现自定义用户登录验证的功能,只需要实现UserDetailsService类,来代替默认的用户信息类来获取用户


@Component
public class MyUserDetailsService  implements UserDetailsService
{
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException
    {
        //这里模拟从数据库跟据名字查用户,我把密码给成了一样,权限给空
        UserDetails userDetails = new User(s,s, Collections.emptyList());
        return userDetails;
    }
}

这个类有个loadUserByUsername方法,就是通过用户账户获取用户,即通过前端提交的用户账户。找到对应的用户,返回一个UserDetails接口对象,注入了MyUserDetailsService后Security 会使用注入的这个类获取登陆用户的UserDetails,如果获取到了,就会用这个UserDetails对象和我们前台提交的用户密码进行对比,判断用户密码信息是否正确。也可以手动配置 比如这样:注释掉@Component 自动注入 增加 .userDetailsService(myUserDetailsService())


    /**
    *注入自定义的用户信息获取器
    */
    @Bean
    public MyUserDetailsService myUserDetailsService(){
        return new MyUserDetailsService();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
//        String redirectUrl = securityProperties.getBrowser().getLoginPage();
        //basic 登录方式
//      http.httpBasic()

        //表单登录 方式
        http.

                formLogin()
//                .loginPage("/login")
                //登录需要经过的url请求
                .loginProcessingUrl("/login/form")
                .successForwardUrl("/test/a")
                .defaultSuccessUrl("/test/b",true)
                //登录成功处理器
                .successHandler(successHandler)
                .failureHandler(failHandler)
                .and()
                //请求授权
                .authorizeRequests()
                //不需要权限认证的url
                .antMatchers("/login","/login/form").permitAll()
                //任何请求
                .anyRequest()
                //需要身份认证
                .authenticated()
                .and()
                ==================这里=====================
                //在这里增加自定义用户过滤器的配置
                .userDetailsService(myUserDetailsService())
                ==================这里=====================
                //关闭跨站请求防护
                .csrf().disable();
    }



或者是重载另外一个configure方法设置

设置用户服务类和密码编码器
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //设置用户信息服务类
        auth.userDetailsService(userService)
        //设置密码编码器
        .passwordEncoder(myPasswordEncode());
    }

这样也可以,启动项目后发现登录是会报错,因为security 的密码默认是BCrypt加密的,自然不会验证通过,并且会提示密码不像个BCrypt加密字符串。有两个办法,一个当然是遵从规则,我们给UserDetails存入BCrypt加密后的密文,这样security拿到前端的密码后进行加密,用加密后的密码和UserDetails中的加密密码进行验证。

修改刚才的获取用户信息代码,这里把账户给加密作为密码,当然真实情况应该是注册用户时把密文存入数据库,BCryptPasswordEncoder这个类就是Security 默认的加密处理类,encode方法就是加密方法

//new BCryptPasswordEncoder().encode(s) 对变量s进行加密,这样就得到了一个加密密码的用户
   UserDetails userDetails = new User(s,new BCryptPasswordEncoder().encode(s), Collections.emptyList());

另一个办法就是重新设置密码的验证规则,这里我提供两个途径去实现

  1. 实现自定义的PasswordEncoder(密码编码器) 替换调Security的默认密码编码器
  2. 实现AuthenticationProvider(身份认证)接口

先说第一个,只需要实现PasswordEncoder接口,并将其配置到Security中, :配置方法见上面的设置用户信息配置链接.


import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 自定义的编码器, 模拟明文的验证方式
 * @Component 注入就会生效
 * @author zhoupan
 * @description
 * @date 2021/8/12 下午10:16
 */
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    /**
     * 加密方法,这个方法是用来对前端密码进行加密的
     * @param charSequence
     * @return
     */
    @Override
    public String encode(CharSequence charSequence)
    {
        //我这里不加密 直接返回原密码
        return charSequence.toString();
    }

    /**
     * 验证方法
     * @param charSequence
     * @param s
     * @return
     */
    @Override
    public boolean matches(CharSequence charSequence, String s)
    {
        //验证密码方式 一样则验证通过
        if(s.equals(charSequence.toString()))
        {
            return true;
        }

        return false;
    }
}

第二个方法,实现AuthenticationProvider后,这时已经不需要配置UserDetailsService的实例了,因为身份认证器已经包括了用户信息和密码认证,都需要在认证方法里去实现,配置了也没有影响,虽然仍然会走默认的密码验证,但是因为我们额外置的AuthenticationProvider,当默认密码验证失败后(这个认证器 DaoAuthenticationProvider),会走我们自己新配的这个身份认证类。当然注释掉是最好的。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

/**
 * 自定义密码验证 支持多个身份验证器
 * @author zhoupan
 * @data 2021/8/12 17:09
 * @description
 */
@Component
public class MyAuthenticationProvider implements AuthenticationProvider
{
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    /**
     *  认证处理,返回一个Authentication的实现类则代表认证成功,返回null则代表认证失败 
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException
    {
        //前端传递的账户和密码
        String username = authentication.getName();
        String presentedPassword = (String)authentication.getCredentials();
        //获取用户信息,这里要换成实际我们的用户信息获取方法
        UserDetails user = myUserDetailsService.loadUserByUsername(username);
        if(user==null)
        {
            throw new BadCredentialsException("账户或密码错误");
        }
        //写自己系统的认证加密等业务逻辑,这里直接判断相等
        if(!presentedPassword.equals(user.getPassword()))
        {
            throw new BadCredentialsException("账户或密码错误");
        }
        //返回一个经过验证的身份对象 ,这里有重载方法 可以将用户的权限信息设置到这个已经经过验证的权限令牌里
        return new UsernamePasswordAuthenticationToken(user, presentedPassword);
    }

    /**
     *
     * 如果这个AuthenticationProvider支持指定的Authentication则返回true 支持进一步认证
     * 这个注释是源码里翻译的我理解半天也没看懂,后来跟了下源码,ProviderManager的 authenticate方法,是认证方法,登陆时
     *UsernamePasswordAuthenticationFilter这个过滤器用账户和密码去构建UsernamePasswordAuthenticationToken对象,注册到ProviderManager中,ProviderManager调用authenticate方法并
     *传入UsernamePasswordAuthenticationToken,方法内会先拿到系统内的所有身份验证器,执行AuthenticationProvider的supports方

     *法判断是否可以处理,如果不能就接着循环,直到找到为true的,调用其authenticate方法,如果得到的身份令牌不为空,则跳出循环,不再进行其他相同的身份认证器认证了(指
     *UsernamePasswordAuthenticationToken身份只要有一个认证成功就成功了) 进行了一个测试看下是否是这样,打开我们刚才的 .
     *userDetailsService(myUserDetailsService()) ,系统会先走这个,刚才已经说过了,失败时会接着走我们自己的验证器,现在让系统的登录成功,测试结果没有再进入我们自定义的了。
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(Class<?> aClass)
    {
        return aClass.equals(UsernamePasswordAuthenticationToken.class);
    }
}



@Component 注入这个类就会生效了,
现在已经实现了自定义登陆了,可以使用Security 默认的加密,和自定义密码验证,这种是利用表单登陆验证,而现在大多都是前后端分离的,前端项目是个独立的工程,如果用户没有登陆,也不会由后端跳转到登陆页,因为后端项目就没有前端页面,而是直接报401,前端项目收到401后自己路由到登陆页。

这种情况,就不需要开启表单登陆,只需要把登陆接口配置成不拦截,在的登陆方法里利用前面提到的ProviderManager 这个类,手动去调用authenticate方法去认证,怎么拿到ProviderManager的实例,刚好我们继承的WebSecurityConfigurerAdapter这个类就有实例化的方法,

    /**
    *通过重写authenticationManagerBean方法,利用@Bean将AuthenticationManager交给spring IOC 容器
    */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }
     @Override
    protected void configure(HttpSecurity http) throws Exception
    {
//        String redirectUrl = securityProperties.getBrowser().getLoginPage();
        //basic 登录方式
//      http.httpBasic()

        //表单登录 方式
        http

//                formLogin()
//                .loginPage("/login")
                //登录需要经过的url请求
//                .loginProcessingUrl("/login/form")
//                .successForwardUrl("/test/a")
//                .defaultSuccessUrl("/test/b",true)
                //登录成功处理器
//                .successHandler(successHandler)
//                .failureHandler(failHandler)
//                .and()
                //请求授权
                .authorizeRequests()
                //不需要权限认证的url ,放开登陆
                .antMatchers("/login").permitAll()
                //任何请求
                .anyRequest()
                //需要身份认证
                .authenticated()
                .and()
                //关闭跨站请求防护
                .csrf().disable();
    }

通过@Bean 我们注入了AuthenticationManager对象,并且注释掉了表单登陆,然后实现一个登陆接口,

    @Autowired
    private AuthenticationManager authenticationManager;

    @RequestMapping("/login")
    private String  login(@RequestParam("userName") String userName,@RequestParam("password") String password)
    {

        System.out.println("账户:"+userName+",密码:"+password);
        Authentication authentication = null;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(userName, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                return "账户或密码错误";
            }
            else
            {
                return "登陆失败";
            }
        }
        return "登陆成功";
    }

这样就实现了自定义登陆接口,通过postman 直接调用接口就可以了
http://localhost:8080/login?userName=aaa&password=aaa
只不过手动调用AuthenticationManager的验证方法后,session 就失效了,下一篇就开始记录下使用token进行身份认证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值