Springboot整合SpringSecurity 06-登陆扩展之自定义登陆验证逻辑

Springboot整合SpringSecurity 06-登陆扩展之自定义登陆验证逻辑

前面我们使用JDBC来验证登陆其实平常开发已经够用了。
但是有时候会有一些特别的需求: 比如需要远程登陆,并不是我们本地的数据库进行账号密码验证,登陆接口在另一台远程服务器上面。
这个时候JDBC的方式已经不能满足我们了。
本章以远程登陆为例,讲解SpringSecurity如何实现自定义远程登陆验证账号密码。

本系列的按顺序写的,如果对于某些代码不清楚,请看下前面的几篇文章。
Springboot整合SpringSecurity 01-使用入门
Springboot整合SpringSecurity 02-使用自定义登陆页面
Springboot整合SpringSecurity 03-访问权限控制
Springboot整合SpringSecurity 04-启用登出logout功能
Springboot整合SpringSecurity 05-使用JDBC实现认证和授权
Springboot整合SpringSecurity 06-登陆扩展之自定义登陆验证逻辑
Springboot整合SpringSecurity 07-方法访问权限控制

1. 新建一个本例的UserDetailsService

/**
 * 远程登陆的时候的UserService。因为是远程登陆。所以这里其实是不需要的。
 * 之前的UserService是获取用户信息交给SpringSecurity去校验。
 * 现在我们不需要SpringSecurity校验,所以这个UserService实现其实是为了骗SpringSecurity。
 * 让他能够验证通过,好能够进行后面的逻辑,也就是我们自己的自定义逻辑。
 *
 * 这里如果不返回有效的userDetails,那么SpringSecurity就会报认证失败,所以这个实现就是个幌子。
 *
 * @author flw
 */
@Service
public class UserService2 implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserVo vo = new UserVo();
        vo.setUsername(username);
        vo.setPassword("{MD5}123456");
        return vo;
    }
}

因为SpringSecurity在账号密码验证前需要先从UserDetailsService根据账号获取userDetails。
但是其实我们远程登陆并不需要这个userDetails,因为我们真正要根据去验证的地方是在远程服务器。
但是如果UserDetailsService返回的是null,那么SpringSecurity会直接报错认证失败。
所以这里我们直接返回一个空白的userVo,说白了就是骗下Spring。

2. 创建登陆验证相关逻辑处理类。

2.1 登陆扩展之AuthenticationDetailsSource(看需求是否需要)

因为SpringSecurity的验证逻辑里面默认是只有本地登陆请求中的用户名密码的,如果你还需要本次请求中的其他参数,那么你就需要向下面这样做。如果不需要额外参数的话,这里可以跳过的。

首先创建一个LoginVo用来保存登陆除了账号密码以外我们需要的参数。本例中我们假设额外有nickName,age。这个等下有用。

public class LoginVo {

    private String nickName;

    private String age;

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

然后,创建一个AuthenticationDetailsSource来创建details。这个后面验证的时候有用。

public class MyWebAuthenticationDetailsSource implements
        AuthenticationDetailsSource<HttpServletRequest, LoginVo> {

    @Override
    public LoginVo buildDetails(HttpServletRequest request) {
        String age = request.getParameter("age");
        LoginVo loginVo = new LoginVo();
        if (StringUtils.isNotBlank(age)) {
            loginVo.setAge(age);
        }
        String nickName = request.getParameter("nickName");
        if (StringUtils.isNotBlank(nickName)) {
            loginVo.setNickName(nickName);
        }
        return loginVo;
    }

}

注意这个实现有两个泛型,第一个泛型HttpServletRequest是SpringSecurity在认证的时候已经确定的,不能修改了,而第二个泛型就是我们的额外信息,这个可以是随便我们的实体类。

2.2 创建登陆校验核心类。
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    public MyAuthenticationProvider(UserDetailsService userDetailsService) {
        setUserDetailsService(userDetailsService);
    }

    /**
     *
     * @param userDetails 是我们的userDetailsService里面获取的数据
     * @param authentication 是请求的账号和密码以及一些额外信息
     * @throws AuthenticationException
     */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
                                                  UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        //这里就是一个例子,表明我们可以从details里面获取很多额外信息
        LoginVo details = (LoginVo) authentication.getDetails();
        System.out.println(details.getAge());
        System.out.println(details.getNickName());

        String username = authentication.getPrincipal().toString();
        String password = authentication.getCredentials().toString();

        List<String> role = loginRemote(username, password);
        if (CollectionUtils.isEmpty(role)) {
            throw new BadCredentialsException("错误的用户名和密码");
        }
        UserVo user = (UserVo) userDetails;
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String s : role) {
            authorities.add(() -> s);
        }
        user.setAuthorities(authorities);
    }

    /**
     * 模拟远程登陆
     *
     * 这里我们实际就是加个本地判断
     * @param username
     * @param password
     */
    private List<String> loginRemote(String username, String password) {
        if ("admin".equals(username) && "adminpwd".equals(password)) {
            return Collections.singletonList("ROLE_ADMIN");
        } else if ("user".equals(username) && "userpwd".equals(password)) {
            return Collections.singletonList("ROLE_USER");
        } else if ("dba".equals(username) && "dbapwd".equals(password)) {
            List<String> list = new ArrayList<>();
            list.add("ROLE_USER");
            list.add("ROLE_DBA");
            return list;
        }
        return null;
    }
}

DaoAuthenticationProvider : SpringSecurity默认的登陆校验处理器。
MyAuthenticationProvider: 因为我们只是更改登陆校验逻辑,所以只需要修改DaoAuthenticationProvider的additionalAuthenticationChecks()方法,所以这里我就选择了继承DaoAuthenticationProvider重写他的验证方法。

additionalAuthenticationChecks:
参数一: userDetails 是我们的userDetailsService里面获取的数据,就是我们的空白UserVo
参数二: authentication 是请求的账号和密码以及一些额外信息,这个额外信息就是我们上面创建的LoginVo

注意: SpringSecurity里面的验证失败等错误都是通过抛出异常来实现的。就是我们上面的BadCredentialsException

3. 修改WebSecurityConfig

@EnableWebSecurity
@MapperScan("com.demo.spring.security.mapper")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter  {

    @Autowired
    private UserDetailsService userService;

    @Autowired
    private UserDetailsService userService2;

//    @Bean
//    @Override
//    public UserDetailsService userDetailsService() {
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        manager.createUser(User.withDefaultPasswordEncoder().username("user")
//                .password("user").roles("USER").build());
//        manager.createUser(User.withDefaultPasswordEncoder().username("admin")
//                .password("admin").roles("ADMIN").build());
//        manager.createUser(User.withDefaultPasswordEncoder().username("dba")
//                .password("dba").roles("DBA","USER").build());
//        return manager;
//    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new MyAuthenticationProvider(userService2));
    }
  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
//                .userDetailsService(userService)
                .userDetailsService(userService2)
                .authorizeRequests()
                .antMatchers("/static/**", "/common/**","/login/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").access("hasRole('USER') and hasRole('DBA')")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .authenticationDetailsSource(new MyWebAuthenticationDetailsSource())
                .successHandler(new MyAuthenticationSuccessHandler())
                .permitAll()
                .and()
                .logout()
//                .logoutUrl("/my/logout")
//                .logoutSuccessUrl("/my/index")
//                .logoutSuccessHandler(null)
                .invalidateHttpSession(true)
//                .addLogoutHandler(null)
                .deleteCookies("testCookie", "testCookie2")
                .permitAll();
    }

}

这里我们主要就修改了以上两点:
一:更改 userDetailsService(userService2)使用我们哪个空白的userService2
二:authenticationProvider(new MyAuthenticationProvider(userService2))配置认证提供者为我们的
MyAuthenticationProvider。

注意我们的MyAuthenticationProvider是在configure(AuthenticationManagerBuilder auth)里面配置的。这里面一定不要使用super.configure(auth);因为我们要重写Manager,不用默认的Provider。

4. 修改登陆页面login.html

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" >
<link rel="stylesheet" type="text/css" th:href="@{/static/css/test.css}"/>
<body>
<h1>This is My Login Page</h1>
<form th:action="@{/login}" method="post">
    <p th:if="${error != null}">
        <span>
            <font class="error">Invalid username and password.</font>
        </span>
    </p>
    <p th:if="${logout != null}">
        <span>You are logout.</span>
    </p>
    <p>
        <label for="username">Username</label>
        <input class="username" type="text" id="username" name="username"/>
    </p>
    <p>
        <label for="password">Password</label>
        <input type="password" id="password" name="password"/>
    </p>
    <p>
        <label for="nickName">nickName</label>
        <input type="text" id="nickName" name="nickName"/>
    </p>
    <p>
        <label for="age">age</label>
        <input type="text" id="age" name="age"/>
    </p>
    <input type="hidden"
           th:name="${_csrf.parameterName}"
           th:value="${_csrf.token}"/>
    <button type="submit" class="btn">Log in</button>
</form>
</body>
</html>

我们在原有的基础上面新增了两个文本框,nickName和age,用来模拟登陆需要除了用户名密码以外参数的场景。

5. 启动项目验证

现在我们的自定义登陆验证逻辑就已经完成了。
启动项目后我们访问登陆页面:

http://localhost:10022/security/login

输入账号密码admin/adminpwd,

然后访问需要ADMIN权限的/admin/hello接口:

http://localhost:10022/security/admin/hello

结果可以成功访问。

然后我们访问需要USER和DBA权限的/user/hello接口

http://localhost:10022/security/user/hello

结果403,项目改造完成。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值