当前Java Web项目中主流的开发模式是前后端分离的模式,而Spring Security默认的登录是由Security框架提供的页面的表单来输入用户名、密码,且由Security框架自动处理登录流程,不适合我们前后端开发的模式,我们后端需要自己开发相关验证登录流程,我们在开发测试时需要对Security 进行初始配置!
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthorizationFilter jwtAuthorizationFilter;
private static Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
public SecurityConfiguration(){
log.debug("加载Security配置类");
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 请求路径白名单
String[] urls = {
"/favicon.ico",
"/doc.html",
"/**/*.js",
"/**/*.css",
"/swagger-resources/**",
"/v2/api-docs",
"/product/login"
};
http.cors(); // 允许跨域访问,关键点在于允许预检请求
http.csrf().disable();//禁用防止伪造攻击
http.authorizeRequests()//要求请求必须被授权
.antMatchers(urls)//匹配路径
.permitAll() // 允许访问
.anyRequest() // 除以上配置以外的请求
.authenticated();// 经过认证的
http.addFilterBefore(jwtAuthorizationFilter,
UsernamePasswordAuthenticationFilter.class);
}
}
过滤器代码段:
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Value("${csmall.jwt.secret-key}")
private String secretKey;
public JwtAuthorizationFilter() {
log.debug("创建过滤器:JwtAuthorizationFilter");
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
log.debug("执行JwtAuthorizationFilter");
// 清除Security上下文中的数据
SecurityContextHolder.clearContext();
// 从请求头中获取JWT
String jwt = request.getHeader("Authorization");
log.debug("从请求头中获取JWT:{}", jwt);
// 判断JWT数据是否基本有效
if (!StringUtils.hasText(jwt) || jwt.length() < 80) {
log.debug("获取到的JWT是无效的,直接放行,交由后续的组件继续处理!");
// 过滤器链继续执行,相当于:放行
filterChain.doFilter(request, response);
// 返回,终止当前方法本次执行
return;
}
// 设置响应结果的文档类型,主要用于处理解析JWT时的异常
response.setContentType("application/json; charset=utf-8");
// 尝试解析JWT
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
} catch (MalformedJwtException e) {
log.warn("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
ServiceCode.ERR_JWT_PARSE, "无法获取到有效的登录信息,请重新登录!");
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (SignatureException e) {
log.warn("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
ServiceCode.ERR_JWT_PARSE, "无法获取到有效的登录信息,请重新登录!");
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (ExpiredJwtException e) {
log.warn("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
ServiceCode.ERR_JWT_EXPIRED, "登录信息已过期,请重新登录!");
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (Throwable e) {
log.warn("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());
JsonResult<Void> jsonResult = JsonResult.fail(
ServiceCode.ERR_JWT_PARSE, "无法获取到有效的登录信息,请重新登录!");
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
}
// 从JWT的解析结果中获取数据
Long id = claims.get("id", Long.class);
String username = claims.get("username", String.class);
String authorityListString = claims.get("authorities", String.class);
log.debug("从JWT中解析得到id:{}", id);
log.debug("从JWT中解析得到username:{}", username);
log.debug("从JWT中解析得到authorities:{}", authorityListString);
// 准备Authentication对象,后续会将此对象封装到Security的上下文中
LoginPrincipal loginPrincipal = new LoginPrincipal();
loginPrincipal.setId(id);
loginPrincipal.setUsername(username);
List<SimpleGrantedAuthority> authorities = JSON.parseArray(
authorityListString, SimpleGrantedAuthority.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(
loginPrincipal, null, authorities);
// 将用户信息封装到Security的上下文中
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
log.debug("已经向Security的上下文中写入:{}", authentication);
// 过滤器链继续执行,相当于:放行
filterChain.doFilter(request, response);
}
}
我们分代码段来解释其中的含义:
@Autowired
private JwtAuthorizationFilter jwtAuthorizationFilter;
首先我们要自动装配一个JWT的认证过滤器,这个JWT就是Json Web Token,现在比较流行的给客户端做标识的技术,基于token
的认证方式相比传统的session
认证方式更节约服务器资源,并且对移动端和分布式更加友好。过去我们还需要用专门的服务器存放Session,存储时间也不长久,如果用cookie呢,只能存在客户端本地,无法跨域。Token就很好的结局了这个问题,我们可以理解它为车票,我们服务端是检票口,我们只负责验证票据。
private static Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
public SecurityConfiguration(){
log.debug("加载Security配置类");
}
这一段是创建本类的日志对象,并在构造方法中输出,以便于我们在控制台观察程序状态。
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
显示配置Bean方法,交由Spring管理这个BCryptPasswordEncoder对象。这个类之前的文章有介绍认证框架SpringSecurity
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
这个是我们继承了 WebSecurityConfigurerAdapter适配器接口之后去重写的认证方法,可以自定义自己的认证方式
@Override
protected void configure(HttpSecurity http) throws Exception {
// 请求路径白名单
String[] urls = {
"/favicon.ico",
"/doc.html",
"/**/*.js",
"/**/*.css",
"/swagger-resources/**",
"/v2/api-docs",
"/product/login"
};
http.cors(); // 允许跨域访问,关键点在于允许预检请求
http.csrf().disable();//禁用防止伪造攻击
http.authorizeRequests()//要求请求必须被授权
.antMatchers(urls)//匹配路径
.permitAll() // 允许访问
.anyRequest() // 除以上配置以外的请求
.authenticated();// 经过认证的
http.addFilterBefore(jwtAuthorizationFilter,
UsernamePasswordAuthenticationFilter.class);
}
这个是我们继承了 WebSecurityConfigurerAdapter适配器接口之后去重写的认证方法,可以自定义自己的认证方式。这里面的具体设置信息为
http.cors(); // 表示允许跨域访问,关键点在于允许预检请求
解释:前端浏览器往往会发出options请求,会报cors preflight关键字样的错误,这个就是为了解决跨域问题。
http.csrf().disable();//禁用防止伪造攻击
解释:跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法,我们往往在开发中将此验证关闭。
http.authorizeRequests()//要求请求必须被授权
.antMatchers(urls)//匹配路径,就是上面数组中的那些地址
.permitAll() // 允许访问所有
.anyRequest() // 除以上配置以外的请求
.authenticated();// 经过认证的
http.addFilterBefore(jwtAuthorizationFilter,
UsernamePasswordAuthenticationFilter.class);
在框架自带的过滤器之前加入我们自己的JWT认证过滤器。