当你阅读这篇文章的时候,我们假设你已经对Spring security有所了解,并且懂得如何初步使用。
前言
- 当你阅读这篇文章的时候,我们假设你已经对Spring security有所了解,并且懂得如何初步使用。
如果不是,你可以先通过其他文档先了解Spring security,以及如何在Spring Boot项目中集成Spring security。 - Spring security是一款过度设计的安全框架,设计者为使用者准备了强大的功能和扩展方法,这导致了它十分复杂和繁琐,无法做到开箱即用,扩展起来也十分麻烦,很容易掉进坑里爬不出来。
- 同时,Spring security只提供了账号密码的登录方法,这在实际项目中往往是不够用的,这需要我们去扩展定制其他的登录的方法,这篇文章会介绍如何扩展登录方法。
- 这篇文章旨在帮助你如何快速地在你的项目中应用Spring security实现登录认证、鉴权的功能,作者水平有限,如有错漏,欢迎指出。
从配置开始搭建
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 添加配置文件
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 此方法配置 认证方法
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
/**
* 过滤器配置方法
*/
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
/**
* http安全配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
从实战场景开始
从这里开始,我们开始配置认证方法,我们假设,实战项目中有多种用户,多种认证方法。
比如我们开发一个一个图书借阅系统,有APP、学生端小程序、管理端小程序、管理后台。
用户类型有:APP 有学生和管理员两种用户 、学生端小程序有学生用户,管理端小程序有管理员用户、管理后台有管理员用户。
这样用户、终端就是这样的关系
好了,一步一步来。
从基础的账号密码开始
基础部分 不会很详细,如果你还不熟悉spring security,建议先从其他文章开始
首先,我们先实现账号密码登录。
账号1
- 定义一个User类:
@Data
public class User {
private String userId;
private String account;
private String password;
}
想要实现认证,就要实现Spring Security的UserDetails接口,所以,先将User类改成这样
@Data
public class User implements UserDetails{
private String userId;
private String account;
private String password;
@Override
@JsonIgnore
public String getPassword() {
return password;
}
@Override
@JsonIgnore
public String getUsername() {
return account;
}
}
当然不要忘记 service
@Service
public class UserService implements AuthUserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 根据账号查询
return null;
}
}
应该记得,Spring Security是通过AuthenticationToken类的辨别该用什么Provider去执行登录逻辑。
默认的账号密码登录就是通过UsernamePasswordAuthenticationToken,执行DaoAuthenticationProvider完成登录。
所以,我们创建类UserUsernamePasswordAuthenticationToken,继承UsernamePasswordAuthenticationToken
public class UserUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
public UserUsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public UserUsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
这么做是为了跟接下来要做的管理员 账号密码登录做区分,避免两种账户的登录混淆了。
- 登录流程
完成前面的工作后,我们开始实现登录过程。
先定义User用的Provider
public class UserAuthenticationProvider extends DaoAuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UserUsernamePasswordAuthenticationToken.class);
}
}
登录成功的处理器
@Component
public class UserPasswordLoginSuccessHandler extends AbstractLoginSuccessHandler {
@Resource
private UserService userService;
@Override
public Object preAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
User user = (User) authentication.getPrincipal();
// do something
return userVO;
}
}
别忘了 filter
public class PasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
protected String username = null, password = null;
private final Class<? extends UsernamePasswordAuthenticationToken> auth;
public PasswordAuthenticationFilter(String loginPattern, Class<? extends UsernamePasswordAuthenticationToken> auth,
AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler) {
super(new AntPathRequestMatcher(loginPattern, "POST"));
setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
this.auth = auth;
this.setAuthenticationSuccessHandler(successHandler);
this.setAuthenticationFailureHandler(failureHandler);
}
@Override
public void afterPropertiesSet() {
Assert.notNull(getAuthenticationManager(), "authenticationManager must be specified");
Assert.notNull(getSuccessHandler(), "AuthenticationSuccessHandler must be specified");
Assert.notNull(getFailureHandler(), "AuthenticationFailureHandler must be specified");
}
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
super.preAuth(request);
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
// 获取参数中的账号密码
if (StringUtils.hasText(body)) {
JsonObject object = JSONUtils.parseObject(body);
username = object.getString("account");
password = object.getString("password");
}
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = auth.getConstructor(Object.class, Object.class).newInstance(username, password);
try {
// 调用登录方法:provider
return this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
} catch (BadCredentialsException b) {
throw new BadCredentialsException("密码错误");
} catch (LockedException l) {
throw new BadCredentialsException("账号已被冻结");
}
}
}
此时的 配置代码如下:
@Configuration
@EnableWebSecurity// 这个注解必须加,开启Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//保证post之前的注解可以使用
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Resource
private UserService userService;
@Resource
private RestAuthenticationAccessDeniedHandler restAuthenticationAccessDeniedHandler;
@Resource
private TokenAuthenticationFailureHandler tokenAuthenticationFailureHandler;
@Resource
private LoginFailureHandler loginFailureHandler;
@Resource
private UserPasswordLoginSuccessHandler userPasswordLoginSuccessHandler;
/**
* Provider
*/
public AuthenticationProvider userAuthentication() {
DaoAuthenticationProvider provider = new UserAuthenticationProvider();
provider.setUserDetailsService(userService);
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(bCryptPasswordEncoder);
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(userAuthentication());
}
// 过滤器
protected List<AbstractAuthenticationProcessingFilter> configFilters() {
List<AbstractAuthenticationProcessingFilter> filters = new ArrayList<>();
filters.add(new PasswordAuthenticationFilter("/user/login/account", UserUsernamePasswordAuthenticationToken.class, userPasswordLoginSuccessHandler, loginFailureHandler));
return filters;
}
@Override
public void configure(WebSecurity web) {
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.accessDeniedHandler(restAuthenticationAccessDeniedHandler)
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.sessionManagement().disable()
.cors()
.and()
.headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(new Header("Access-control-Allow-Origin", "*"), new Header("Access-Control-Expose-Headers", "generateAuthorization"))))
.and()
.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
//配置token认证
.apply(new JwtAuthorizeConfigurer<>())
.addJwtAuthenticationFilter(JwtAuthenticationFilter.create("/user/**", UserJwtAuthenticationToken.class, null, tokenAuthenticationFailureHandler,
"/user/login/account"))
//配置登陆
.and()
.apply(new LoginConfigurer<>(configFilters()))
.and()
;
}
}
至此完成了User账号密码登录配置。
先定义User用的Provider
public class UserAuthenticationProvider extends DaoAuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UserUsernamePasswordAuthenticationToken.class);
}
}
登录成功的处理器
@Component
public class UserPasswordLoginSuccessHandler extends AbstractLoginSuccessHandler {
@Resource
private UserService userService;
@Override
public Object preAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
User user = (User) authentication.getPrincipal();
// do something
return userVO;
}
}
别忘了 filter
public class PasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
protected String username = null, password = null;
private final Class<? extends UsernamePasswordAuthenticationToken> auth;
public PasswordAuthenticationFilter(String loginPattern, Class<? extends UsernamePasswordAuthenticationToken> auth,
AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler) {
super(new AntPathRequestMatcher(loginPattern, "POST"));
setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
this.auth = auth;
this.setAuthenticationSuccessHandler(successHandler);
this.setAuthenticationFailureHandler(failureHandler);
}
@Override
public void afterPropertiesSet() {
Assert.notNull(getAuthenticationManager(), "authenticationManager must be specified");
Assert.notNull(getSuccessHandler(), "AuthenticationSuccessHandler must be specified");
Assert.notNull(getFailureHandler(), "AuthenticationFailureHandler must be specified");
}
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
super.preAuth(request);
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
// 获取参数中的账号密码
if (StringUtils.hasText(body)) {
JsonObject object = JSONUtils.parseObject(body);
username = object.getString("account");
password = object.getString("password");
}
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = auth.getConstructor(Object.class, Object.class).newInstance(username, password);
try {
// 调用登录方法:provider
return this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
} catch (BadCredentialsException b) {
throw new BadCredentialsException("密码错误");
} catch (LockedException l) {
throw new BadCredentialsException("账号已被冻结");
}
}
}
此时的 配置代码如下:
@Configuration
@EnableWebSecurity// 这个注解必须加,开启Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//保证post之前的注解可以使用
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Resource
private UserService userService;
@Resource
private RestAuthenticationAccessDeniedHandler restAuthenticationAccessDeniedHandler;
@Resource
private TokenAuthenticationFailureHandler tokenAuthenticationFailureHandler;
@Resource
private LoginFailureHandler loginFailureHandler;
@Resource
private UserPasswordLoginSuccessHandler userPasswordLoginSuccessHandler;
/**
* Provider
*/
public AuthenticationProvider userAuthentication() {
DaoAuthenticationProvider provider = new UserAuthenticationProvider();
provider.setUserDetailsService(userService);
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(bCryptPasswordEncoder);
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(userAuthentication());
}
// 过滤器
protected List<AbstractAuthenticationProcessingFilter> configFilters() {
List<AbstractAuthenticationProcessingFilter> filters = new ArrayList<>();
filters.add(new PasswordAuthenticationFilter("/user/login/account", UserUsernamePasswordAuthenticationToken.class, userPasswordLoginSuccessHandler, loginFailureHandler));
return filters;
}
@Override
public void configure(WebSecurity web) {
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.accessDeniedHandler(restAuthenticationAccessDeniedHandler)
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.sessionManagement().disable()
.cors()
.and()
.headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(new Header("Access-control-Allow-Origin", "*"), new Header("Access-Control-Expose-Headers", "generateAuthorization"))))
.and()
.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
//配置token认证
.apply(new JwtAuthorizeConfigurer<>())
.addJwtAuthenticationFilter(JwtAuthenticationFilter.create("/user/**", UserJwtAuthenticationToken.class, null, tokenAuthenticationFailureHandler,
"/user/login/account"))
//配置登陆
.and()
.apply(new LoginConfigurer<>(configFilters()))
.and()
;
}
}
至此完成了User账号密码登录配置。
多账号模式
账号2
我们创建一个新的账号类Admin
- 定义一个Admin类:
@Data
public class Admin {
private String adminId;
private String account;
private String password;
}
其他步骤省略,创建类AdminUsernamePasswordAuthenticationToken,继承UsernamePasswordAuthenticationToken
public class AdminUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
public AdminUsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
public AdminUsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
- 登录流程
Admin用的Provider
public class AdminAuthenticationProvider extends DaoAuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(AdminUsernamePasswordAuthenticationToken.class);
}
}
注意supports方法里的参数
登录成功的处理器
@Component
public class AdminPasswordLoginSuccessHandler extends AbstractLoginSuccessHandler {
@Resource
private AdminService adminService;
@Override
public Object preAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
Admin admin = (Admin) authentication.getPrincipal();
return admin;
}
}
此时的 配置代码如下:
@Configuration
@EnableWebSecurity// 这个注解必须加,开启Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//保证post之前的注解可以使用
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Resource
private UserService userService;
@Resource
private RestAuthenticationAccessDeniedHandler restAuthenticationAccessDeniedHandler;
@Resource
private TokenAuthenticationFailureHandler tokenAuthenticationFailureHandler;
@Resource
private LoginFailureHandler loginFailureHandler;
@Resource
private UserPasswordLoginSuccessHandler userPasswordLoginSuccessHandler;
/**
* Provider
*/
public AuthenticationProvider userAuthentication() {
DaoAuthenticationProvider provider = new UserAuthenticationProvider();
provider.setUserDetailsService(userService);
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(bCryptPasswordEncoder);
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(userAuthentication());
}
// 过滤器
protected List<AbstractAuthenticationProcessingFilter> configFilters() {
List<AbstractAuthenticationProcessingFilter> filters = new ArrayList<>();
filters.add(new PasswordAuthenticationFilter("/user/login/account", UserUsernamePasswordAuthenticationToken.class, userPasswordLoginSuccessHandler, loginFailureHandler));
return filters;
}
@Override
public void configure(WebSecurity web) {
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.accessDeniedHandler(restAuthenticationAccessDeniedHandler)
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.sessionManagement().disable()
.cors()
.and()
.headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(new Header("Access-control-Allow-Origin", "*"), new Header("Access-Control-Expose-Headers", "generateAuthorization"))))
.and()
.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
//配置token认证
.apply(new JwtAuthorizeConfigurer<>())
.addJwtAuthenticationFilter(JwtAuthenticationFilter.create("/user/**", UserJwtAuthenticationToken.class, null, tokenAuthenticationFailureHandler,
"/user/login/account"))
//配置登陆
.and()
.apply(new LoginConfigurer<>(configFilters()))
.and()
;
}
}
至此完成了User账号密码登录配置。
先定义User用的Provider
public class UserAuthenticationProvider extends DaoAuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UserUsernamePasswordAuthenticationToken.class);
}
}
登录成功的处理器
@Component
public class UserPasswordLoginSuccessHandler extends AbstractLoginSuccessHandler {
@Resource
private UserService userService;
@Override
public Object preAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
User user = (User) authentication.getPrincipal();
// do something
return userVO;
}
}
别忘了 filter
public class PasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
protected String username = null, password = null;
private final Class<? extends UsernamePasswordAuthenticationToken> auth;
public PasswordAuthenticationFilter(String loginPattern, Class<? extends UsernamePasswordAuthenticationToken> auth,
AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler) {
super(new AntPathRequestMatcher(loginPattern, "POST"));
setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
this.auth = auth;
this.setAuthenticationSuccessHandler(successHandler);
this.setAuthenticationFailureHandler(failureHandler);
}
@Override
public void afterPropertiesSet() {
Assert.notNull(getAuthenticationManager(), "authenticationManager must be specified");
Assert.notNull(getSuccessHandler(), "AuthenticationSuccessHandler must be specified");
Assert.notNull(getFailureHandler(), "AuthenticationFailureHandler must be specified");
}
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
super.preAuth(request);
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
// 获取参数中的账号密码
if (StringUtils.hasText(body)) {
JsonObject object = JSONUtils.parseObject(body);
username = object.getString("account");
password = object.getString("password");
}
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = auth.getConstructor(Object.class, Object.class).newInstance(username, password);
try {
// 调用登录方法:provider
return this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
} catch (BadCredentialsException b) {
throw new BadCredentialsException("密码错误");
} catch (LockedException l) {
throw new BadCredentialsException("账号已被冻结");
}
}
}
此时的 配置代码如下:
@Configuration
@EnableWebSecurity// 这个注解必须加,开启Security
@EnableGlobalMethodSecurity(prePostEnabled = true)//保证post之前的注解可以使用
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Resource
private AdminService adminService;
@Resource
private UserService userService;
@Resource
private RestAuthenticationAccessDeniedHandler restAuthenticationAccessDeniedHandler;
@Resource
private TokenAuthenticationFailureHandler tokenAuthenticationFailureHandler;
/**
* Handler
*/
@Resource
private LoginFailureHandler loginFailureHandler;
@Resource
private AdminPasswordLoginSuccessHandler adminPasswordLoginSuccessHandler;
@Resource
private UserPasswordLoginSuccessHandler userPasswordLoginSuccessHandler;
/**
* Provider
*/
public AuthenticationProvider adminAuthentication() {
DaoAuthenticationProvider provider = new AdminAuthenticationProvider();
provider.setUserDetailsService(adminService);
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(bCryptPasswordEncoder);
return provider;
}
public AuthenticationProvider userAuthentication() {
DaoAuthenticationProvider provider = new UserAuthenticationProvider();
provider.setUserDetailsService(userService);
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(bCryptPasswordEncoder);
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth
.authenticationProvider(adminAuthentication())
.authenticationProvider(userAuthentication())
;
}
// 登录过滤器链
protected List<AbstractAuthenticationProcessingFilter> configFilters() {
List<AbstractAuthenticationProcessingFilter> filters = new ArrayList<>();
filters.add(new PasswordAuthenticationFilter("/admin/login/account", AdminUsernamePasswordAuthenticationToken.class, adminPasswordLoginSuccessHandler, loginFailureHandler));
filters.add(new PasswordAuthenticationFilter("/user/login/account", UserUsernamePasswordAuthenticationToken.class, userPasswordLoginSuccessHandler, loginFailureHandler));
return filters;
}
@Override
public void configure(WebSecurity web) {
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.accessDeniedHandler(restAuthenticationAccessDeniedHandler)
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.sessionManagement().disable()
.cors()
.and()
.headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(new Header("Access-control-Allow-Origin", "*"), new Header("Access-Control-Expose-Headers", "generateAuthorization"))))
.and()
.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
//配置token认证
.apply(new JwtAuthorizeConfigurer<>())
.addJwtAuthenticationFilter(JwtAuthenticationFilter.create("/admin/**", AdminJwtAuthenticationToken.class, null, tokenAuthenticationFailureHandler,
"/admin/login/account"))
.addJwtAuthenticationFilter(JwtAuthenticationFilter.create("/user/**", UserJwtAuthenticationToken.class, null, tokenAuthenticationFailureHandler,
"/user/login/account"))
//配置登陆
.and()
.apply(new LoginConfigurer<>(configFilters()))
.and()
;
}
}
至此,我们完成了双账号的账号密码登录模式。
简单总结
spring security是通过token类控制登录流程的,因此 不管有多少个账号类,只要每个账号都有对应的一个token类,并且有对应的provider(实际上并不需要,但账号密码登录我们目前使用spring security的方法,因此才需要)、successHandler 即可。
登录方式扩展
未完待续…