疯狂创客圈 Java 高并发【 亿级流量聊天室实战】实战系列 【博客园总入口

架构师成长+面试必备之 高并发基础书籍 【Netty Zookeeper Redis 高并发实战


前言

Crazy-SpringCloud 微服务脚手架 &视频介绍

Crazy-SpringCloud 微服务脚手架,是为 Java 微服务开发 入门者 准备的 学习和开发脚手架。并配有一系列的使用教程和视频,大致如下:

高并发 环境搭建 图文教程和演示视频,陆续上线:

Linux Redis 安装(带视频)Linux Redis 安装(带视频)
Linux Zookeeper 安装(带视频)Linux Zookeeper 安装, 带视频
Windows Redis 安装(带视频)Windows Redis 安装(带视频)
RabbitMQ 离线安装(带视频)RabbitMQ 离线安装(带视频)
ElasticSearch 安装, 带视频ElasticSearch 安装, 带视频
Nacos 安装(带视频)Nacos 安装(带视频)

Crazy-SpringCloud 微服务脚手架 图文教程和演示视频,陆续上线:

EurekaEureka 入门,带视频
SpringCloud Configspringcloud Config 入门,带视频
spring securityspring security 原理+实战
Spring SessionSpringSession 独立使用
分布式 session 基础RedisSession (自定义)
重点: springcloud 开发脚手架springcloud 开发脚手架
SpingSecurity + SpringSession 死磕 (写作中)SpingSecurity + SpringSession 死磕

小视频以及所需工具的百度网盘链接,请参见 疯狂创客圈 高并发社群 博客

Spring Security 的重要性

在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。Spring Security 是spring项目之中的一个安全模块,特别是在spring boot项目中,spring security已经默认集成和启动了。

Spring Security 默认为自动开启的,可见其重要性。

如果要关闭,需要在启动类加上,exclude ={SecurityAutoConfiguration} 的配置

@EnableEurekaClient
@SpringBootApplication(scanBasePackages = {
"com.crazymaker.springcloud.user",
"com.crazymaker.springcloud.seckill.remote.fallback",
"com.crazymaker.springcloud.standard"
}, exclude = {SecurityAutoConfiguration.class})

或者

spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

一般不建议关闭。

Spring Security 核心组件

spring security核心组件有:Userdetails 、Authentication,UserDetailsService、AuthenticationProvider、AuthenticationManager 下面分别介绍。

Authentication

authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。Authentication也是一个接口,其定义如下:

public interface Authentication extends Principal, Serializable {

Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

接口有4个get方法,分别获取

  • Authorities, 填充的是用户角色信息。

  • Credentials,直译,证书。填充的是密码。

  • Details ,用户信息。

  • Principal 直译,形容词是“主要的,最重要的”,名词是“负责人,资本,本金”。感觉很别扭,所以,还是不翻译了,直接用原词principal来表示这个概念,其填充的是用户名。

    因此可以推断其实现类有这4个属性。

这几个方法作用如下:

  • getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。

  • getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。

  • getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)

  • getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。

  • isAuthenticated: 获取当前 Authentication 是否已认证。

  • setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

UserDetails

UserDetails,看命知义,是用户信息的意思。其存储的就是用户信息,其定义如下:

public interface UserDetails extends Serializable {

Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}

方法含义如下:

  • getAuthorites:获取用户权限,本质上是用户的角色信息。

  • getPassword: 获取密码。

  • getUserName: 获取用户名。

  • isAccountNonExpired: 账户是否过期。

  • isAccountNonLocked: 账户是否被锁定。

  • isCredentialsNonExpired: 密码是否过期。

  • isEnabled: 账户是否可用。

UserDetailsService

提到了UserDetails就必须得提到UserDetailsService, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername,他可以用来获取UserDetails。


package org.springframework.security.core.userdetails; public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);方法。我们在实现loadUserByUsername方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails,(通常是一个org.springframework.security.core.userdetails.User,它继承自UserDetails) 并返回。

在实现loadUserByUsername方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException。

AuthenticationProvider

负责真正的验证。

当我们使用 authentication-provider 元素来定义一个 AuthenticationProvider 时,如果没有指定对应关联的 AuthenticationProvider 对象,Spring Security 默认会使用 DaoAuthenticationProvider。DaoAuthenticationProvider 在进行认证的时候需要一个 UserDetailsService 来获取用户的信息 UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证的方式,我们可以实现自己的 AuthenticationProvider;如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。

实现了自己的 AuthenticationProvider 之后,我们可以在配置文件中这样配置来使用我们自己的 AuthenticationProvider。其中 myAuthenticationProvider 就是我们自己的 AuthenticationProvider 实现类对应的 bean。

AuthenticationProvider 接口如下:

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException; boolean supports(Class<?> var1);
}
  • authenticate 表示认证的动作。

  • supports 表示所支持的 Authentication类型。Authentication 包含很多子类,如果 AbstractAuthenticationToken 。

AbstractAuthenticationToken implements Authentication

还有,可以自定义 Authentication ,比如 本实例所使用的: JwtAuthenticationToken。

AuthenticationManager

认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider。

AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication,其定义如下:

public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}

AuthenticationManager 的作用就是校验Authentication,如果验证失败会抛出AuthenticationException异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候。

组件比较多,但是如果主要流程理顺了,也比较简单。

Spring Security 实战

搞定两个 AuthenticationProvider:

(1) 从数据库获取用户

首先通过 UserDetailsService 获取 UserDetails,然后 通过 UserDetailsService 装配 DaoAuthenticationProvider

(2) 完成用户的认证

实现一个自己的 JwtAuthenticationProvider,完成用户的认证

(3)定制一个过滤器

(4)完成所有组件的装配

实战1 : UserDetailsService 获取 UserDetails

首先通过 UserDetailsService 获取 UserDetails,然后 通过 UserDetailsService 装配 DaoAuthenticationProvider。

package com.crazymaker.springcloud.user.info.service.impl;

@Slf4j
@Service
public class UserAuthService implements UserDetailsService { private PasswordEncoder passwordEncoder; public UserAuthService() {
//默认使用 bcrypt, strength=10
this.passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
} private UserPO loadFromDB(String username) {
if (null == userDao) {
userDao = CustomAppContext.getBean(UserDao.class);
} List<UserPO> list = userDao.findAllByLoginName(username); if (null == list || list.size() <= 0) {
return null;
}
UserPO userPO = list.get(0);
return userPO;
} @Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException { UserPO userPO = loadFromDB(username); //将salt放到password字段返回
return User.builder()
.username(userPO.getLoginName())
.password(userPO.getPassword())
// .password(SessionConstants.SALT)
//BCrypt.gensalt(); 正式开发时可以调用该方法实时生成加密的salt
// .password(SessionConstants.SALT)
.authorities(SessionConstants.USER_INFO)
.roles("USER")
.build(); } }

实战2: 装配 DaoAuthenticationProvider

在 SecurityConfiguration 配置类中加入如下内容:


@Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception {
//这里会默认使用BCryptPasswordEncoder比对加密后的密码,注意要跟createUser时保持一致
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
daoProvider.setUserDetailsService(userDetailsService());
return daoProvider;
} @Override
protected UserDetailsService userDetailsService() {
return new UserAuthService();
}

实战3: 实现一个自己的 JwtAuthenticationProvider

继承于 AuthenticationProvider,实现一个自己的 JwtAuthenticationProvider,完成用户的认证

package com.crazymaker.springcloud.standard.security.provider;
//... public class JwtAuthenticationProvider implements AuthenticationProvider { private RedisOperationsSessionRepository sessionRepository;
private CustomedSessionIdResolver httpSessionIdResolver; public JwtAuthenticationProvider(RedisOperationsSessionRepository sessionRepository,
CustomedSessionIdResolver httpSessionIdResolver) {
this.sessionRepository = sessionRepository;
this.httpSessionIdResolver = httpSessionIdResolver;
} @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
DecodedJWT jwt = ((JwtAuthenticationToken) authentication).getToken();
if (jwt.getExpiresAt().before(Calendar.getInstance().getTime())) {
throw new NonceExpiredException("认证过期");
}
String sid = jwt.getSubject();
String otoken = jwt.getToken(); Session session = null; try {
session = sessionRepository.findById(sid);
} catch (Exception e) {
e.printStackTrace();
} if (null == session) {
throw new NonceExpiredException("认证有误,请重新登录");
} String json = session.getAttribute(G_USER);
if (StringUtils.isBlank(json)) {
throw new NonceExpiredException("认证有误,请重新登录");
} UserDTO userDTO = JsonUtil.jsonToPojo(json, UserDTO.class);
if (null == userDTO) {
throw new NonceExpiredException("认证有误");
} String password = userDTO.getPassword(); String username = userDTO.getLoginName();
UserDetails user = User.builder()
.username(username)
.password(password)
.authorities(SessionConstants.USER_INFO)
.build(); String encryptSalt = password;
try {
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(sid).build();
verifier.verify(jwt.getToken());
} catch (Exception e) {
throw new BadCredentialsException("JWT token verify fail", e);
}
JwtAuthenticationToken token =
new JwtAuthenticationToken(user, jwt, user.getAuthorities());
return token;
} @Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
} }

实战4: 装配 AuthenticationManager

认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider。

  @EnableWebSecurity()
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter { @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider())
.authenticationProvider(jwtAuthenticationProvider());
}
//.... }

实战5: 定制过滤器,将 AuthenticationManager 用起来

搞得再多,如果不通过过滤器,将 AuthenticationManager 用起来,也是没有用的。

package com.crazymaker.springcloud.standard.security.filter;
//..... public class JwtAuthenticationFilter extends OncePerRequestFilter { private RequestMatcher requiresAuthenticationRequestMatcher;
private List<RequestMatcher> permissiveRequestMatchers;
private AuthenticationManager authenticationManager; private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); //.....
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication authResult = null;
/**
* 场景: 从 zuul 过来,直接带上session 头
*/
if (StringUtils.isNotEmpty(request.getHeader(SessionConstants.SESSION_SEED))) {
request.setAttribute(SessionConstants.SESSION_SEED,
request.getHeader(SessionConstants.SESSION_SEED));
UserDetails userDetails = User.builder()
.username(request.getHeader(SessionConstants.SESSION_SEED))
.password(request.getHeader(SessionConstants.SESSION_SEED))
.authorities(SessionConstants.USER_INFO)
.build();
authResult = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
successfulAuthentication(request, response, filterChain, authResult); filterChain.doFilter(request, response);
return;
} /**
* 正常场景: 单体微服务访问,或者从Zuul过来,没有带 session head
*/
if (!requiresAuthentication(request, response)) { filterChain.doFilter(request, response);
return;
}
AuthenticationException failed = null;
try {
String token = getJwtToken(request);
if (StringUtils.isNotBlank(token)) {
JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));
DecodedJWT jwt = authToken.getToken(); //将 AuthenticationManager 用起来
authResult = this.getAuthenticationManager().authenticate(authToken);
UserDetails user = (UserDetails) authResult.getPrincipal();
request.setAttribute(SessionConstants.SESSION_SEED, jwt.getSubject());
} else {
failed = new InsufficientAuthenticationException("请求头认证消息为空");
}
} catch (JWTDecodeException e) {
logger.error("JWT format error", e);
failed = new InsufficientAuthenticationException("请求头认证消息格式错误", failed);
} catch (InternalAuthenticationServiceException e) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
failed = e;
} catch (AuthenticationException e) {
// Authentication failed
failed = e;
}
if (authResult != null) {
successfulAuthentication(request, response, filterChain, authResult);
} else if (!permissiveRequest(request)) {
unsuccessfulAuthentication(request, response, failed);
return;
} filterChain.doFilter(request, response);
} protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
} //.... }

实战6: 配置 HttpSecurity 的过滤机制

还是在 UserWebSecurityConfig 配置文件,将 HttpSecurity 的过滤机制配置起来,完成所有组件的装配。

代码如下:

package com.crazymaker.springcloud.user.info.config;
//... @EnableWebSecurity()
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource
private UserAuthService userAuthService; protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/api/user/login/v1",
// "/api/user/add/v1",
// "/api/user/speed/test/v1",
// "/api/user/say/hello/v1",
// "/api/user/*/detail/v1",
"/api/crazymaker/duty/info/user/login")
.permitAll()
.anyRequest().authenticated() // .antMatchers("/image/**").permitAll()
// .antMatchers("/admin/**").hasAnyRole("ADMIN")
.and() .formLogin().disable()
.sessionManagement().disable()
.cors()
.and() .addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
.apply(new JsonLoginConfigurer<>()).loginSuccessHandler(jsonLoginSuccessHandler())
.and()
.apply(new JwtAuthConfigurer<>()).tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissiveRequestUrls("/logout")
.and()
.logout()
// .logoutUrl("/logout") //默认就是"/logout"
.addLogoutHandler(tokenClearLogoutHandler())
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.and()
.addFilterBefore(springSessionRepositoryFilter(), SessionManagementFilter.class)
.sessionManagement().disable()
; } @Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/api/user/login/v1",
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
// "/api/user/say/hello/v1",
// "/api/user/add/v1",
// "/api/user/speed/test/v1",
// "/api/user/*/detail/v1",
"/images/**",
"/swagger-ui.html",
"/webjars/**",
"**/favicon.ico",
"/css/**",
"/js/**",
"/api/crazymaker/info/user/login"
); } @Resource
RedisOperationsSessionRepository sessionRepository; @Resource
public CustomedSessionIdResolver httpSessionIdResolver; @DependsOn({"sessionRepository", "httpSessionIdResolver"})
@Bean("jwtAuthenticationProvider")
protected AuthenticationProvider jwtAuthenticationProvider() {
return new JwtAuthenticationProvider(sessionRepository, httpSessionIdResolver);
} public <S extends Session> OncePerRequestFilter springSessionRepositoryFilter() { CustomedSessionRepositoryFilter<? extends Session> sessionRepositoryFilter = new CustomedSessionRepositoryFilter<>(
sessionRepository);
// sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
} @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider())
.authenticationProvider(jwtAuthenticationProvider());
} @Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
} @Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception {
//这里会默认使用BCryptPasswordEncoder比对加密后的密码,注意要跟createUser时保持一致
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
daoProvider.setUserDetailsService(userDetailsService());
return daoProvider;
} @Bean
protected JwtRefreshSuccessHandler jwtRefreshSuccessHandler() {
return new JwtRefreshSuccessHandler();
} @Override
protected UserDetailsService userDetailsService() {
return new UserAuthService();
} @Bean
protected JsonLoginSuccessHandler jsonLoginSuccessHandler() {
return new JsonLoginSuccessHandler(userAuthService);
} @Bean
protected TokenClearLogoutHandler tokenClearLogoutHandler() {
return new TokenClearLogoutHandler(userAuthService);
} @Bean
protected CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "HEAD", "OPTION"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.addExposedHeader(SessionConstants.AUTHORIZATION);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
} }

实战小结

大概通过以上6步,一个集成jwt的springsecurity机制,完整的配置起来了。

具体,请关注 Java 高并发研习社群博客园 总入口


最后,介绍一下疯狂创客圈:疯狂创客圈,一个Java 高并发研习社群博客园 总入口

疯狂创客圈,倾力推出:面试必备 + 面试必备 + 面试必备 的基础原理+实战 书籍 《Netty Zookeeper Redis 高并发实战

spring security 原理+实战-LMLPHP


疯狂创客圈 Java 死磕系列

  • Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战

Java 面试题 一网打尽**


05-16 15:32