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,项目改造完成。