【SpringSecurity-3】Springboot + SpringSecurity + Oauth2授权码模式

上一篇我们实现了授权码的整个流程,并且添加了资源服务器,通过获取到的token去访问资源服务器提供的REST接口,现在对第一篇中提到的几个小任务进行调研修改下。

(4)获取授权码的用户登录页面是否可以定制,如何操作?

1、授权登陆页面自定义实现

(1)授权登陆页修改

第一步修改security配置文件。

获取授权码时修改登录页面,需要对授权服务器配置进行修改,修改如下:

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll()
            .and()
            .logout().permitAll();

    // 新增:自定义登录页面
    // http.formLogin()
    // 属性1 - loginPage() : 用户配置登录页面url地址
    //      (1)可以直接配置一个url  例如:loginPage("http://192.168.1.200/pc-admin")
    //      (2)配置项目中的页面   例如:
    http.formLogin()
            .loginPage("/login.html");

    http.csrf().disable();
}

第二步添加login.html

pom.xml文件中引入 spring-boot-starter-thymeleaf 依赖

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

在resources文件夹下创建static文件夹并创建login.html文件

login.html文件如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>

<style>
    .login-container {
        margin: 50px;
        width: 100%;
    }

    .form-container {
        margin: 0px auto;
        width: 50%;
        text-align: center;
        box-shadow: 1px 1px 10px #888888;
        height: 300px;
        padding: 5px;
    }

    input {
        margin-top: 10px;
        width: 350px;
        height: 30px;
        border-radius: 3px;
        border: 1px #E9686B solid;
        padding-left: 2px;

    }


    .btn {
        width: 350px;
        height: 35px;
        line-height: 35px;
        cursor: pointer;
        margin-top: 20px;
        border-radius: 3px;
        background-color: #E9686B;
        color: white;
        border: none;
        font-size: 15px;
    }

    .title{
        margin-top: 5px;
        font-size: 18px;
        color: #E9686B;
    }
</style>
<body>
<div class="login-container">
    <div class="form-container">
        <p class="title">用户登录</p>
        <form name="loginForm" method="post" th:action="login">
            <input type="text" name="username" placeholder="用户名"/>
            <br>
            <input type="password" name="password" placeholder="密码"/>
            <br>
            <button type="submit" class="btn">登 &nbsp;&nbsp; 录</button>
        </form>
        <p style="color: red" th:if="${param.error}">用户名或密码错误</p>
    </div>
</div>
</body>
</html>

这样我们就自定义了一个登陆页面,并且将登陆页面集成到security中

(2)用户名密码动态设置

我们自定义了登陆页面,但是用户名/密码 是我们在内存中配置的,在实际项目中的意义不大,我们需要将用户名密码持久化,通过匹配数据库中的用户名密码进行校验。实现自定义的用户名密码验证,有一下两种方式,第一种是直接定义一个表单登陆的AuthorizationProvider,然后再AuthenticationManagerBuilder中使用我们定义的表单登陆AuthorizationProvider;第二种是通过定义一个filter,在security的过滤链中的UsernamePasswordAuthenticationFilter之前执行。第一种方式实现比较简单,先来看第一种实现。

第一种实现方式

首先自定义FormLoginAuthenticationProvider

@Component
public class FormLoginAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private CustomUserDetailService customUserDetailService;

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取用户输入的用户名和密码
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        UserDetails user = customUserDetailService.loadUserByUsername(username);
        // 密码校验
        String outp = user.getPassword();

        if (!outp.equals(password)) {
            throw new UsernameNotFoundException("用户名/密码错误!");
        }
        return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    }

    public boolean supports(Class<?> aClass) {
        return true;
    }
}

①FormLoginAuthenticationProvider实现了AuthenticationProvider类,并实现该类的authenticate(Authentication authentication)方法和supports(Class aClass)方法,通过方法名我们就可以知道,authenticate(Authentication authentication)方法是用来执行用户认证的;supprot(Class aClass)是用来进行判断该provider支持那种类型的用户校验。

@该Provider中我们注入了一个CustomUserDetailService 这个类,这个是我们定义的用来加载用户信息的一个类。

③authenticate()方法中的逻辑就是进行用户名密码校验了。

接下来我们定义CustomUserDetailService

@Component
public class CustomUserDetailService implements UserDetailsService {

    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        // 只是为了实验,这里的用户可以通过读取数据库获取
        if (!"1".equals(s) && !"2".equals(s) && !"3".equals(s) && !"4".equals(s)) {
            throw new UsernameNotFoundException("用户不存在");
        }
        List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
        list.add(new SimpleGrantedAuthority("admin"));

        User user = new User(s, "123456", list);
        return user;
    }
}

①该方法实现了UserDetailsService方法,实现用户自定义查询

②这里我们写死了用户名/密码,我们这里做验证没有加入数据库信息,项目中这里的用户可以通过读取数据库获取用户信息

③经过校验这里返回了一个内置的user对象

定义好provider,我们在security配置 (SecurityConfig.java)中引入FormLoginProvider进行我们自定义的用户校验

@Autowired
public FormLoginAuthenticationProvider formLoginAuthenticationProvider;

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    /*BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    auth.inMemoryAuthentication().withUser("admin")
            .password(encoder.encode("123456"))
            .authorities("ROLE_ADMIN");*/
    auth.authenticationProvider(formLoginAuthenticationProvider);
}

①之前我们定义的用户名密码是放在内存中了,现在我们通过自定义provider实现我们自己的用户逻辑判断。

这样我们就实现了自己的用户密码认证。

有个小问题就是登录页的错误信息没有返回展示出来。这里我们修改下

添加两个接口,一个登录接口,一个登录失败接口

SecurityController

@Controller
@RequestMapping("api")
public class SecurityController {

    @GetMapping("login")
    public String login() {
        return "login";
    }

    @PostMapping("loginerr")
    public String loginerr(Model mode) {
        mode.addAttribute("err", true);
        return "login";
    }
}

①使用@Controller接口时为了配合使用thymeleaf,不然返回String 是返回的一个字符串,不是指定的页面

②其他的就是定义一个登录接口,跳转到login.html;一个登录失败的接口,同样跳转到login.html,只不过失败时会带一个参数,返回给前端login.html,用于提示错误信息,这里不能展示详细的信息,如果想要展示详细信息可以自定义successhandle 和 failhandle

SecurityConfig中formLogin的loginPage需要修改下,同时添加failureForwardUrl

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll()
            .and()
            .logout().permitAll();

    // 自定义登录页面

    // http.formLogin()
    // 属性1 - loginPage() : 用户配置登录页面url地址
    //      (1)可以直接配置一个url  例如:loginPage("http://192.168.1.200/pc-admin")
    //      (2)配置项目中的页面   例如:
    http.formLogin()
            .loginPage("/api/login") //我们定义的登录接口
            .failureForwardUrl("/api/loginerr"); //我们定义的失败接口

    http.csrf().disable();
}

小知识点:

springboot中static文件夹和templates文件夹中的区别:

Springboot的static和templates - 汪神 - 博客园

到此,方式一就算完成了。

第二种实现方式

我们自定义实现一个filter,使该filter在usernamepasswordfilter之前执行。

首先我们定义一个FormLoginAuthenticationFilter

public class FormLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * 构造方法
     * 实现父类构造方法,匹配拦截 /login url
     *
     **/
    public FormLoginAuthenticationFilter() {
        super(new AntPathRequestMatcher("/api/login", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {

        String username = httpServletRequest.getParameter("username");
        String password = httpServletRequest.getParameter("password");

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password, null);
        return this.getAuthenticationManager().authenticate(authenticationToken);
    }

①FormLoginAuthenticationFilter继承 AbstractAuthenticationProcessingFilter 实现其中最重要的一个方法: attemptAuthentication()

②attemptAuthentication()中会调用AuthenticationManager -> ProviderManager 中的 authenticate()方法,这一步时security中最核心的一部分,后面会说到。这里先知道这个点。

③实现一个无参的构造方法,指定了拦截 “/api/login” url,该方法定义了该filter的主要作用就是拦截form的登录接口。这里的“/api/login”是我们在SecurityConfig中定义的formLogin().loginPage("/api/login")

然后我们定义提供认证的provider

FormLoginAuthentication2Provider 直接复制的上面的FormLoginAuthenticationProvider

@Component
public class FormLoginAuthentication2Provider implements AuthenticationProvider {

    @Autowired
    private CustomUserDetailService customUserDetailService;

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取用户输入的用户名和密码
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        UserDetails user = customUserDetailService.loadUserByUsername(username);
        // 密码校验
        String outp = user.getPassword();

        if (!outp.equals(password)) {
            throw new UsernameNotFoundException("用户名/密码错误!");
        }
        return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    }

    public boolean supports(Class<?> aClass) {
        return true;
    }
}

修改SecurityConfig配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    // start 新增   定义FormLoginAuthenticationFilter,设置AuthenticationManager和FailureHandler
    FormLoginAuthenticationFilter filter = new FormLoginAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManagerBean());
    filter.setAuthenticationFailureHandler(new ForwardAuthenticationFailureHandler("/api/loginerr"));
    // end
    
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll()
            .and()
            .logout().permitAll();

    // 自定义登录页面

    // http.formLogin()
    // 属性1 - loginPage() : 用户配置登录页面url地址
    //      (1)可以直接配置一个url  例如:loginPage("http://192.168.1.200/pc-admin")
    //      (2)配置项目中的页面   例如:formLogin().loginPage("login.html")

    // 自定义接口,实现页面错误提示,如果想要达到详细的错误信息提示,
    //  可以实现类AuthenticationSuccessHandler重写onAuthenticationSuccess方法 和
    //  实现AuthenticationFailureHandler类重写onAuthenticationFailure方法
    http.formLogin()
            .loginPage("/api/login")
            .failureForwardUrl("/api/loginerr");

    http.csrf().disable();
    
    // start 新增  添加filter的执行位置,在UsernamePasswordAuthenticationFilter前执行
    http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
    // end
    
    
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        /*BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        auth.inMemoryAuthentication().withUser("admin")
                .password(encoder.encode("123456"))
                .authorities("ROLE_ADMIN");*/
//        auth.authenticationProvider(formLoginAuthenticationProvider);
        // start 配置认证Provider
        auth.authenticationProvider(formLoginAuthentication2Provider);
    }

配置中做了三部分处理:

①定义了我们自己实现的FormLoginAuthenticationFilter ,并且给filter设置了AuthenticationManager和AuthenticationFailureHandler,如果不设置AuthenticationFailureHandler默认使用的是SimpleUrlAuthenticationFailureHandler

这里我们认证失败后需要跳转到我们定义的失败接口,所以我们需要让我们的filter配置ForwardAuthenticationFailureHandler该处理器,跳转到我们定义的“/api/loginerr”失败接口。

②设置我们定义的filter的执行位置,我们放在了UsernamePasswordAuthenticationFilter的前面,先进行我们判断,失败后直接返回了,不用再进行UsernamePasswordAuthenticationFilter的认证流程了。

③配置AuthenticationProvider,实现最终的认证逻辑。

两种方式比较

通过这两种方式,不管是哪种方式,最根本的用户校验是通过我们定义的 Provider (formLoginAuthentication2Provider, formLoginAuthenticationProvider)。第二种方式 虽然看起来多几个了filter有点罗嗦了,但是给我们提供了一种思路,我们可以扩展实现我们自己的认证方式,比如实现手机号验证码登陆方式,我们在filter中组织 Authentication 返回,然后在Provider中进行手机号验证码验证,从而实现符合我们自己业务的认证方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dabing9

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值