学习springSecurity之前,首先要了解springSecurity能够干些什么事情?能够处理哪些东西?
参考
1、https://blog.csdn.net/qq_22172133/category_8615344.html
2、https://blog.csdn.net/wangooo/article/details/113922946 写的很不错的
3、https://blog.csdn.net/weixin_38927257/article/details/102960752
4、https://blog.csdn.net/weixin_38927257/article/details/103118760#comments_14887493 自定义短信登陆验证
用户认证 Authentication
用户授权 Authonization
底层使用技术及实现原理
SpringSecurity本质就是一个过滤器链;
框架的基本设计模式
Java Servlet 和 Spring Security 都使用了设计模式中的 责任链模式 。简单地说,它们都定义了许多过滤器(Filter),每一个请求都会经过层层过滤器的处理,最终返回。如下图:
其中,Spring Security 在 Servlet 的过滤链(filter chain)中注册了一个过滤器 FilterChainProxy,它会把请求代理到 Spring Security 自己维护的多个过滤链,每个过滤链会匹配一些 URL,如图中的 /foo/**,如果匹配则执行对应的过滤器。过滤链是有顺序的,一个请求只会执行第一条匹配的过滤链。
org.springframework.security.web.FilterChainProxy
1、在springboot-autoconfigure自动配置类中
org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration自动配置类中
@Bean
@Order(2147483642)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
return (SecurityFilterChain)http.build();
}
2、在org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration类中
@Bean({"org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.httpSecurity"})
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {//完成了对HttpSecurity的初始化动作
LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
DefaultPasswordEncoderAuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(this.objectPostProcessor, passwordEncoder);
authenticationBuilder.parentAuthenticationManager(this.authenticationManager());
HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, this.createSharedObjects());
http.csrf(Customizer.withDefaults()).addFilter(new WebAsyncManagerIntegrationFilter()).exceptionHandling(Customizer.withDefaults()).headers(Customizer.withDefaults()).sessionManagement(Customizer.withDefaults()).securityContext(Customizer.withDefaults()).requestCache(Customizer.withDefaults()).anonymous(Customizer.withDefaults()).servletApi(Customizer.withDefaults()).logout(Customizer.withDefaults()).apply(new DefaultLoginPageConfigurer());
return http;
}
认证流程图
框架处理流程
既然我们用这个框架是需要解决用户鉴定和用户授权等应用场景的,那么从框架处理流程入手,可以快速让我们了解这个框架的内容:
框架核心类之间的关系:
当了解完了该框架的设计模式和处理流程后,我们进一步看看该框架是用了哪些核心类来完成以上功能的(这里不需要完全看懂,你也应该不能完全看懂,在后面实现具体功能时弄不清类之间的关系了,就回来看这个图)。
SpringSecurity启动的时候就加载的过滤器
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.authentication.www.BasicAuthenticationFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
org.springframework.security.web.access.intercept.FilterSecurityInterceptor源码解析
//实现了import javax.servlet.Filter;【一个标准的过滤器】
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
//执行过滤器
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
invoke(new FilterInvocation(request, response, chain));
}
//执行返回的FilterInvocation filterInvocation
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// first time this request being called, so perform security checking
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//在执行过滤器之前【做的事情】
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
//过滤执行
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
//过滤器finally【做的事情】
super.finallyInvocation(token);
}
//在执行过滤器后【做的事情】
super.afterInvocation(token, null);
}
}
org.springframework.security.web.access.ExceptionTranslationFilter 处理认证异常处理器
也是实现了Filter接口(一个标准的过滤器)
核心方法
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
//执行过滤器
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
//执行过滤器
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
//异常处理器
handleSpringSecurityException(request, response, chain, securityException);
}
}
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 用户密码认证
通过代码可以发现在取默认的
//用户名
request.getParameter("username");
//密码
request.getParameter("password");
DEFAULT_ANT_PATH_REQUEST_MATCHER 默认的匹配路径是/login,需要使用POST方式提交
private boolean postOnly = true;//默认只能使用POST提交
集成核心
1、在真实开发环境中,不可能使用默认的springSecurityPassword处理器,肯定要获取用户名和密码然后通过数据库查询出来
2、然后重写UsernamePasswordAuthenticationFilter的三个方法
org.springframework.security.core.userdetails.UserDetailsService用户详情服务接口
如果需要实现自定义逻辑的时候,只需要实现UserDetailsService接口即可
org.springframework.security.crypto.password.PasswordEncoder 密码加密器
@Bean public PasswordEncoder passwordEncoder(){ return new Md5Util(); }
配置密码加密器(配置问Md5)
public class Md5Util implements PasswordEncoder {...}实现了PasswordEncoder接口
Web权限解决方法(如何认证以及授权)
1、通过用户名和密码进行登录的过程就是认证过程
2、访问某个资源的时候,进行权限验证的就叫授权
(1)认证
1、如何设置用户名和密码
(1)通过配置文件【不适用】
#设置springSecurity的用户名和密码
spring.security.user.name=wolf_user
spring.security.user.password=wolfbaba
(2)通过配置类【不适用】
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String wolfPass = passwordEncoder.encode("wolfPass");
auth.inMemoryAuthentication().withUser("wolf").password(wolfPass).roles("superAdmin");
}
}
(3)自定义一个实现类(实现UserDetailsService)【常用】
在实际操作过程中,直接定义UserDeatilService实现类即可
(4)通过最底层,在执行UserDeatilService的逻辑处,直接重新构造其实用类
public class CommonAuthenticationProvider implements AuthenticationProvider {
private PasswordEncoder passwordEncoder;
private MessageSourceAccessor messages = new MessageSourceAccessor(new SpringSecurityMessageSource(), Locale.CHINA);//SpringSecurityMessageSource.getAccessor();
private UserDetailsServiceExtension userDetailsService;
private UserDetailsChecker preAuthenticationChecks = new CommonAuthenticationProvider.DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new CommonAuthenticationProvider.DefaultPostAuthenticationChecks();
public CommonAuthenticationProvider(PasswordEncoder passwordEncoder, UserDetailsServiceExtension userDetailsService) {
this.passwordEncoder = passwordEncoder;
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
UserDetails userDetails = this.retrieveUser(username,authentication);
this.preAuthenticationChecks.check(userDetails);
if(authentication instanceof UsernamePasswordAuthenticationToken){
this.additionalAuthenticationChecks(userDetails,authentication);
}
this.postAuthenticationChecks.check(userDetails);
return this.createSuccessAuthentication(authentication, userDetails);
}
private void additionalAuthenticationChecks(UserDetails userDetails, Authentication token) throws AuthenticationException {
if (token.getCredentials() == null) {
log.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = token.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
log.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
private UserDetails retrieveUser(String username, Authentication token) throws AuthenticationException {
try {
UsernamePasswordAuthenticationToken authenticationToken;
if(token instanceof UsernamePasswordAuthenticationToken){
authenticationToken = (UsernamePasswordAuthenticationToken)token;
}else{
authenticationToken = (UsernamePasswordAuthenticationToken)token.getPrincipal();
}
//获取前端传递的参数
String type = ((Map<String, String>)authenticationToken.getDetails()).get("type");
UserDetails loadedUser = this.userDetailsService.loadUserByUsername(username,type);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException e) {
throw e;
} catch (Exception e) {
throw new InternalAuthenticationServiceException(e.getMessage(), e);
}
}
private Authentication createSuccessAuthentication(Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(), user.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> aClass) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass)
|| PreAuthenticatedAuthenticationToken.class.isAssignableFrom(aClass);
}
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
private DefaultPostAuthenticationChecks() {
}
@Override
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
log.debug("User account credentials have expired");
throw new CredentialsExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
}
}
}
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
private DefaultPreAuthenticationChecks() {
}
@Override
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
log.debug("User account is locked");
throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
} else if (!user.isEnabled()) {
log.debug("User account is disabled");
throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
} else if (!user.isAccountNonExpired()) {
log.debug("User account is expired");
throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
}
}
public interface UserDetailsServiceExtension {
/**
* 添加type参数,用与判断是哪类用户登陆
*
* @param username 用户名
* @param type 用户类型
* @return 用户详情
* @throws UsernameNotFoundException 用户不存在
*/
UserDetails loadUserByUsername(String username, String type) throws UsernameNotFoundException;
}
//项目信息(部分核心不展示)
@Service
@AllArgsConstructor
public class UserDetailsServiceExtensionImpl implements UserDetailsServiceExtension {
private AuthUserService userService;
//为了提升效率:启用缓存(登录默认从缓存查询,缓存中没有时,在通过数据库查询)
@Override
@Cacheable(value = SecurityConstants.USER_CACHE_KEY,key = "#username",unless = "#result=null")
public UserDetails loadUserByUsername(String username, String type) throws UsernameNotFoundException {
return this.buildUser(userService.getUserInfoByName(username));
}
private AuthUser buildUser(UserInfo userInfo) {
if (null == userInfo) {
throw new UsernameNotFoundException("用户不存在");
}
Set<String> authSet = new HashSet<>();
if (CollUtil.isNotEmpty(userInfo.getRoles())) {
userInfo.getRoles().forEach(i -> authSet.add(SecurityConstants.ROLE_PREFIX + i));
}
if(CollUtil.isNotEmpty(userInfo.getPermissions())){
authSet.addAll(userInfo.getPermissions());
}
Collection<? extends GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(authSet.toArray(new String[0]));
return new AuthUser(userInfo.getId(),userInfo.getType(), userInfo.getUsername(), userInfo.getPassword(),
StrUtil.equals(CommonConstants.STATUS_NORMAL, userInfo.getDelFlag()),
true, true,
StrUtil.equals(CommonConstants.STATUS_NORMAL, userInfo.getLockFlag()), grantedAuthorities);
}
}
在启动类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tfysy.common.security.extension.CommonAuthenticationProvider
自定义登录页面(自定义用户和密码参数)及过滤不需要进行认证即可访问的页面
通过在配置类中,配置相关参数
重写WebSecurityConfigurerAdapter的void configure(HttpSecurity http) 方法
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin()
.loginProcessingUrl("/login")//登录访问路径(请求地址)--直接请求那个controller
.loginPage("/login.html")//表单登录(登录页面地址)
.defaultSuccessUrl("/main").permitAll()//登录成功,访问的路径
;
//哪些路径可以直接访问,不需要认证
http.authorizeRequests().antMatchers("/login","/login.html").permitAll();
http.httpBasic();
http.csrf().disable();//关闭跨域
}
FormLoginConfigurer表单登录配置
//初始化的时候就构造了UsernamePasswordAuthenticationFilter 用户密码认证过滤器
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
//父类中的authenticationFilter=UsernamePasswordAuthenticationFilter
protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {
this();
this.authFilter = authenticationFilter;
if (defaultLoginProcessingUrl != null) {
loginProcessingUrl(defaultLoginProcessingUrl);
}
}
这个AuthFilter就是UsernamePasswordAuthenticationFilter
也就是最终修改的UsernamePasswordAuthenticationFilter的userName和userPassword的输入参数值
(2)授权(基于角色或者权限进行控制访问)
基于配置文件进行修改
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin()
//.usernameParameter("把名字给改了")
//.passwordParameter("把输入密码参数给改了")
.loginProcessingUrl("/login")//登录访问路径(请求地址)--直接请求那个controller
.loginPage("/login.html")//表单登录(登录页面地址)
.defaultSuccessUrl("/main").permitAll()//登录成功,访问的路径
;
//哪些路径可以直接访问,不需要认证【在这里自定义资源权限控制】
http.authorizeRequests()
.antMatchers("/login","/login.html").permitAll()
//.antMatchers("/main").hasAuthority("wolf_admin")
//.antMatchers("/main").hasAnyAuthority("wolf_admin","wolf_china")
.antMatchers("/main").hasRole("WOLF")
;
http.httpBasic();
//【403无权限控制自定义】
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
// .accessDeniedPage()
;
http.csrf().disable();//关闭跨域
}
}
1)、hasAuthority 如果当前登录用户,具有指定权限,如果有就返回true,没有就返回false
注意:
这里的grantedAuthorities不仅仅包含了资源权限还包含角色权限;只是角色权限用是ROLE_角色名称
访问设置的路径,只有具有wolf_admin权限的,都给你访问
2)、hasAnyAuthority 当前登录用户,只要拥有给定权限随便哪个都允许访问
3)、hasRole和hasAnyRole 是否拥有某个角色或者在给定角色中随便能匹配一个都允许访问
意思跟上面1-2的差不多
注意:
4)、自定义403访问页面或者返回接口
需要通过在配置类中进行配置
1)、自定义权限处理的handler
@Slf4j
@Component
public class ResourceAccessDeniedHandler implements AccessDeniedHandler {
private static final ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
}
/**
* UTF-8
*/
String UTF8 = "UTF-8";
/**
* JSON 资源
*/
String JSON_CONTENT_TYPE = "application/json; charset=utf-8";
@Override
@SneakyThrows
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
log.debug("授权失败,禁止访问 {}", request.getRequestURI());
response.setCharacterEncoding(UTF8);
response.setContentType(JSON_CONTENT_TYPE);
R<String> result = new R<>();
result.setCode(HttpStatus.FORBIDDEN.value());
result.setMsg("权限不足");
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().append(objectMapper.writeValueAsString(result));
}
}
在config配置中加入这个
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
//.accessDeniedPage("/403页面") 403出现后,到某个页面地址
springSecurity注解使用
1、@Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀ROLE_
使用注解必须先要开启
在Spring启动类或者配置文件类,开启
@Configuration
//开启注解使用功能
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
在Controller加入
@Secured("ROLE_WOLF")//判断是否有WOLF这个角色
@GetMapping("/notLogin")
public String notLogin(){
return "我的第一个springSecurity例子";
}
2、@PreAuthorize
注解启用
//开启注解使用功能 @EnableGlobalMethodSecurity(prePostEnabled = true)
适合进入方法前的权限验证,@PreAuthorize可以将登陆用户的roles/permissions参数穿到方法中
@PreAuthorize("hasAnyAuthority('doUpdate')")//拥有这个权限的时候【进入方法之前验证】
//@PreAuthorize("hasRole('ROLE_WOLF')")//拥有这个角色的时候【进入方法之前验证】
@GetMapping("/hasAuthorize")
public String hasAuthorize(){
return "我的第一个springSecurity例子hasAuthorize;";
}
3、@PostAuthorize
注解启用
//开启注解使用功能 @EnableGlobalMethodSecurity(prePostEnabled = true)
在方法执行后在进行权限验证,适合验证带有返回值的权限【先执行方法,然后在进行拦截】
用户注销功能
1)在配置类中添加退出的路径和退出成功后的访问页面或者具体执行的handler
记住我,自动登录
一、先说设置cookie和保存token到数据库中
UsernamePasswordAuthenticationFilter
--》attemptAuthentication父类的方法
--》AbstractAuthenticationProcessingFilter.doFilter
--》attemptAuthentication调用了UsernamePasswordAuthenticationFilter的attemptAuthentication方法
--》successfulAuthentication如果成功,调用授权成功的方法
this.rememberMeServices.loginSuccess(request, response, authResult)调用了记录用户的服务
默认实现是NullRememberMeServices
--》找到rememberMeService抽象类AbstractRememberMeServices
调用了抽象方法onLoginSuccess
--》PersistentTokenBasedRememberMeServices.onLoginSuccess实现
this.tokenRepository.createNewToken(persistentToken);//使用tokenRepository存储器进行存储
--》InMemoryTokenRepositoryImpl是tokenRepository的默认实现
如果在容器中存在BeanType=tokenRepository,spring容器会自动注入这个bean
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//tokenRepository.setCreateTableOnStartup(true);
//启动时建立这张表persistent_logins,也可以吧创建表语句拿去手动执行
//只能执行一次,第二次需要关掉,否则报错MySQLSyntaxErrorException: Table 'persistent_logins' already exists
return tokenRepository;
}
addCookie(persistentToken, request, response);
--》TokenBasedRememberMeServices.onLoginSuccess 这个底层就是放cookie
addCookie(persistentToken, request, response);
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug(LogMessage.format("Creating new persistent login for user %s", username));
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),
generateTokenData(), new Date());
try {
this.tokenRepository.createNewToken(persistentToken);
addCookie(persistentToken, request, response);
}
catch (Exception ex) {
this.logger.error("Failed to save persistent token ", ex);
}
}
二、通过cookie携带的信息,通过tokenRepository存储器数据库查询并验证,然后返回认证结果
RememberMeAuthenticationFilter
属性rememberMeServices默认实现就是
--》Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
AbstractRememberMeServices调用的抽象来的autoLogin方法
1)、取出cookie
2)、登录 UserDetails user = processAutoLoginCookie(cookieTokens, request, response)
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries)
数据库模式时,JdbcTokenRepositoryImpl调用的getTokenForSeries方法
返回了UserDetails=最终调用这个sql语句:select username,series,token,last_used from persistent_logins where series = ?把用户信息查出来
UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
但是在实际运用中
processAutoLoginCookie方法的实现(自动登录)
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2 + " tokens, but contained '"
+ Arrays.asList(cookieTokens) + "'");
}
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
// No series match, so we can't authenticate using this cookie
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
}
// We have a match for this user/series combination
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete all logins for this user and throw
// an exception to warn them.
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage(
"PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
// Token also matches, so login is valid. Update the token value, keeping the
// *same* series number.
this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'",
token.getUsername(), token.getSeries()));
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
generateTokenData(), new Date());
try {
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
addCookie(newToken, request, response);
}
catch (Exception ex) {
this.logger.error("Failed to update token: ", ex);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
return getUserDetailsService().loadUserByUsername(token.getUsername());//底层又调用了userDetailsService.loadUserByUsername去自动完成登录认证
}
三、如何实现
1、在开始分析源码的时候,看到了
1、首先的创建一个基于数据的JdbcTokenRepositoryImpl实现(springSecurity底层已经实现好了的)只是bean需要自行创建,这样才会自动注入到具体使用实例中
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);//设置数据源,在步骤4完成数据源配置后,springboot容器启动由于Auto**Config会自动创建对应的数据源实例
//tokenRepository.setCreateTableOnStartup(true);
//启动时建立这张表persistent_logins,也可以吧创建表语句拿去手动执行
//只能执行一次,第二次需要关掉,否则报错MySQLSyntaxErrorException: Table 'persistent_logins' already exists
return tokenRepository;
}
2、在配置类中,加入rememberMe
http.rememberMe()
.tokenRepository(persistentTokenRepository)//加入token持久化存储器(这个实例就是上面的实例bean),默认实现是基于内存的InMemoryTokenRepositoryImpl
//.tokenValiditySeconds()//秒为单位,token过期时间
.userDetailsService(userDetailsService);//加入userDetailSerivce实例,如果不给的话,系统就找不到自动登录的认证器
3、创建表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4、加入数据源DataSource
在配置文件中,创建数据源
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.username=root
spring.datasource.hikari.password=123456
5、在pom加入数据库驱动
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
6、在登陆页面加入
<input name="remember-me" type="checkbox" value="true"> 下次自动登录
(1)名字必须固定remember-me
PersistentTokenBasedRememberMeServices的父类AbstractRememberMeServices
public static final String DEFAULT_PARAMETER = "remember-me";//明确定义了,参数值为这个
CSRF跨域请求
微服务权限框架
微服务之间,独立部署独立运行,一次授权多处使用;这里就需要使用到单点登录
一般生产环境都是基于token来使用的。为什么了,因为session有些浏览器会禁用session导致客户端体验不好
核心配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//根据需要重写,需要配置的核心配置方法即可
}
认证过滤器
整个登录过程中,拿到用户名和密码后,获取用户信息并判断密码是否正确,然后给权限过滤器返回用户信息;
如果要自定义需要重写UsernamePasswordAuthenticationFilter类的相关方法
认证成功后会调用父类AbstractAuthenticationProcessingFilter的successfulAuthentication方法(认证成功)
认证失败会调用父类AbstractAuthenticationProcessingFilter的unsuccessfulAuthentication方法(认证失败)
既然要基于redis存储,简单了三。把思路驴清楚!!!
1、用户如果认证成功,那么获取认证成功的用户信息,然后根据用户信息生产JWT的token,然后把token保存到redis、数据库或者其他缓存数据库或者缓存中
注意:登录成功后返回一个token给前端,后面前端只需要拿到请求的token就可以来获取认证信息;
授权过滤器
1、从请求header把里面携带的token拿到。然后解析得到具体的用户名称信息;
2、然后根据用户信息通过认证过程存储的用户权限信息拿出来;
3、然后把这些东西交给springSecurity,通过springSecurity给当前这个用户授予对应的权限;
4、实现BasicAuthenticationFilter最基础的权限过滤器接口,然后重写doFilterInternal方法;(方法实现思路,跟1-3步描述思路一致)
下面是自定义的授权过滤器
//根据request获取请求头里面携带的token,然后通过对token进行解析,拿到用户userName,然后在通过username去redis里面查询,如果找到了,把返回用户认证信息
想???
这个地方为什么没使用身份管理器进行验证呢?因为这个地方既然是自定义,我这里直接可以通过token,解析后直接能够拿到用户信息,然后直接通过redis缓存加载权限信息,然后通过
设置用户信息,注意这多了一个权限信息参数
自定义UserDetailService实现,主要是通过用户名查询密码,然后生成User对象返回给springSecurity,然后springSecuriy就能拿到这个用户的认证信息
@Service
public class WolfExtendUserDetailService implements UserDetailsService {
/**
* 角色前缀
*/
String ROLE_PREFIX="ROLE_";
/**
* 删除
*/
String STATUS_DEL = "Y";
/**
* 正常
*/
String STATUS_NORMAL = "N";
/**
* 锁定
*/
String STATUS_LOCK = "Y";
/**
* 实现
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("客户端输入的用户名:" + username);
UserInfo userInfo = new UserInfo();
userInfo.setUsername("zw");
userInfo.setPassword(Md5Util.MD5Encode("123456"));
userInfo.setLockFlag("N");
userInfo.setDelFlag("N");
userInfo.setPermissions(Arrays.asList("wolf_admin","wolf_wifi"));
userInfo.setRoles(Arrays.asList("WOLF"));
//这里就从数据库里面根据前端提供的username把用户信息给查询出来,然后封装springSecurity需要的User对象,返回即可
return buildUser(userInfo);
}
private AuthUser buildUser(UserInfo userInfo) {
if (null == userInfo) {
throw new UsernameNotFoundException("用户不存在");
}
Set<String> authSet = new HashSet<>();
if (CollUtil.isNotEmpty(userInfo.getRoles())) {
userInfo.getRoles().forEach(i -> authSet.add(ROLE_PREFIX + i));
}
if(CollUtil.isNotEmpty(userInfo.getPermissions())){
authSet.addAll(userInfo.getPermissions());
}
Collection<? extends GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(authSet.toArray(new String[0]));
return new AuthUser(userInfo.getId(),userInfo.getType(), userInfo.getUsername(), userInfo.getPassword(),
StrUtil.equals(STATUS_NORMAL, userInfo.getDelFlag()),
true, true,
StrUtil.equals(STATUS_NORMAL, userInfo.getLockFlag()), grantedAuthorities);
}
}
配置网关跨域
网关配置文件
流程分析
认证流程
跟最上面的流程是一致的
源码分析
AbstractAuthenticationProcessingFilter
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//判断请求是否合法例如POST提交,如果不是直接放行
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//获取身份认证器
//attemptAuthentication这个是一个抽象方法,由子类UsernamePasswordAuthenticationFilter实现【想:也就是如果说我们需要自己实现获取身份认证器,那么我们就通过实现这个AbstractAuthenticationProcessingFilter即可】
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
//sessionStrategy策略
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//认证成功干的事情
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed认证失败干的事情
unsuccessfulAuthentication(request, response, ex);
}
}
权限访问流程
1、异常处理ExceptionTranslationFilter
2、权限过滤FilterSecurityInterceptor
请求的资源能否进行访问的权限判断
请求间认证共享
过滤器链执行入口
FilterChainProxy
有哪些过滤器连
additionalFilters = {ArrayList@8244} size = 15
0 = {WebAsyncManagerIntegrationFilter@6668}
1 = {SecurityContextPersistenceFilter@6667}
2 = {HeaderWriterFilter@6666}
3 = {LogoutFilter@6657}
4 = {LoginTokenFilter@6656} //自定义的登录过滤器
5 = {UsernamePasswordAuthenticationFilter@6655}
6 = {TokenAuthFilter@6653} //自定义的授权过滤器
7 = {BasicAuthenticationFilter@8248}
8 = {RequestCacheAwareFilter@8249}
9 = {SecurityContextHolderAwareRequestFilter@8250}
10 = {RememberMeAuthenticationFilter@8251}
11 = {AnonymousAuthenticationFilter@8252}
12 = {SessionManagementFilter@8253}
13 = {ExceptionTranslationFilter@8254}
14 = {FilterSecurityInterceptor@8255}