-
疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备 【博客园总入口 】
-
疯狂创客圈 经典图书 : 《SpringCloud、Nginx高并发核心编程》 大厂必备 + 大厂必备 + 大厂必备 【博客园总入口 】
-
入大厂+涨工资必备: 高并发【 亿级流量IM实战】 实战系列 【 SpringCloud nginx秒杀】 实战系列 【博客园总入口 】
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 高并发实战》
疯狂创客圈 Java 死磕系列
- Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战
- Netty 源码、原理、JAVA NIO 原理
- Java 面试题 一网打尽
- 疯狂创客圈 【 博客园 总入口 】
Java 面试题 一网打尽**
- 疯狂创客圈 【 博客园 总入口 】