https://www.jianshu.com/p/d5ce890c67f7

上一篇博客讲了如何使用Shiro和JWT做认证和授权(传送门:https://www.jianshu.com/p/0b1131be7ace),总的来说shiro是一个比较早期和简单的框架,这个从最近已经基本不做版本更新就可以看出来。这篇文章我们讲一下如何使用更加流行和完整的spring security来实现同样的需求。

Spring Security的架构

按照惯例,在使用之前我们先讲一下简单的架构。不知道是因为spring-security后出来还是因为优秀的设计殊途同归,对于核心模块,spring-security和shiro有80%以上的设计相似度。所以下面介绍中会多跟shiro做对比,如果你对shiro不了解也没关系,跟shiro对比的部分跳过就好。

spring-security中核心概念

  • AuthenticationManager, 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManagerauthenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。这个类基本等同于shiro的SecurityManager
  • AuthenticationProvider, 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider。这个是不是和shiro的Realm的定义很像?基本上你可以帮他们当成同一个东西。按照Spring一贯的作风,主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。

    前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager
  • UserDetailService, 用户认证通过Provider来做,所以Provider需要拿到系统已经保存的认证信息,获取用户信息的接口spring-security抽象成UserDetailService。虽然叫Service,但是我更愿意把它认为是我们系统里经常有的UserDao
  • AuthenticationToken, 所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如最容易理解的UsernamePasswordAuthenticationToken。这个就不多讲了,连名字都跟Shiro中一样。
  • SecurityContext,当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过SecurityUtils.getSubject()到达同样的目的。

    我们大概通过一个认证流程来认识下上面几个关键的概念

     
    认证流程

对web系统的支持

毫无疑问,对于spring框架使用最多的还是web系统。对于web系统来说进入认证的最佳入口就是Filter了。spring security不仅实现了认证的逻辑,还通过filter实现了常见的web攻击的防护。

常用Filter

下面按照request进入的顺序列举一下常用的Filter:

  • SecurityContextPersistenceFilter,用于将SecurityContext放入Session的Filter
  • UsernamePasswordAuthenticationFilter, 登录认证的Filter,类似的还有CasAuthenticationFilter,BasicAuthenticationFilter等等。在这些Filter中生成用于认证的token,提交到AuthenticationManager,如果认证失败会直接返回。
  • RememberMeAuthenticationFilter,通过cookie来实现remember me功能的Filter
  • AnonymousAuthenticationFilter,如果一个请求在到达这个filter之前SecurityContext没有初始化,则这个filter会默认生成一个匿名SecurityContext。这在支持匿名用户的系统中非常有用。
  • ExceptionTranslationFilter,捕获所有Spring Security抛出的异常,并决定处理方式
  • FilterSecurityInterceptor, 权限校验的拦截器,访问的url权限不足时会抛出异常

    Filter的顺序

    既然用了上面那么多filter,它们在FilterChain中的先后顺序就显得非常重要了。对于每一个系统或者用户自定义的filter,spring security都要求必须指定一个order,用来做排序。对于系统的filter的默认顺序,是在一个FilterComparator类中定义的,核心实现如下。
    FilterComparator() {
int order = 100;
put(ChannelProcessingFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
put(WebAsyncManagerIntegrationFilter.class, order);
order += STEP;
put(SecurityContextPersistenceFilter.class, order);
order += STEP;
put(HeaderWriterFilter.class, order);
order += STEP;
put(CorsFilter.class, order);
order += STEP;
put(CsrfFilter.class, order);
order += STEP;
put(LogoutFilter.class, order);
order += STEP;
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order);
order += STEP;
put(X509AuthenticationFilter.class, order);
order += STEP;
put(AbstractPreAuthenticatedProcessingFilter.class, order);
order += STEP;
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
order);
order += STEP;
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order);
order += STEP;
put(UsernamePasswordAuthenticationFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
filterToOrder.put(
"org.springframework.security.openid.OpenIDAuthenticationFilter", order);
order += STEP;
put(DefaultLoginPageGeneratingFilter.class, order);
order += STEP;
put(ConcurrentSessionFilter.class, order);
order += STEP;
put(DigestAuthenticationFilter.class, order);
order += STEP;
put(BasicAuthenticationFilter.class, order);
order += STEP;
put(RequestCacheAwareFilter.class, order);
order += STEP;
put(SecurityContextHolderAwareRequestFilter.class, order);
order += STEP;
put(JaasApiIntegrationFilter.class, order);
order += STEP;
put(RememberMeAuthenticationFilter.class, order);
order += STEP;
put(AnonymousAuthenticationFilter.class, order);
order += STEP;
put(SessionManagementFilter.class, order);
order += STEP;
put(ExceptionTranslationFilter.class, order);
order += STEP;
put(FilterSecurityInterceptor.class, order);
order += STEP;
put(SwitchUserFilter.class, order);
}

对于用户自定义的filter,如果要加入spring security 的FilterChain中,必须指定加到已有的那个filter之前或者之后,具体下面我们用到自定义filter的时候会说明。

JWT认证的实现

关于使用JWT认证的原因,上一篇介绍Shiro的文章中已经说过了,这里不再多说。需求也还是那3个:

  • 支持用户通过用户名和密码登录
  • 登录后通过http header返回token,每次请求,客户端需通过header将token带回,用于权限校验
  • 服务端负责token的定期刷新

    下面我们直接进入Spring Secuiry的项目搭建。

项目搭建

gradle配置

最新的spring项目开始默认使用gradle来做依赖管理了,所以这个项目也尝试下gradle的配置。除了springmvc和security的starter之外,还依赖了auth0的jwt工具包。JSON处理使用了fastjson。

buildscript {
ext {
springBootVersion = '2.0.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
} apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management' group = 'com.github.springboot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8 repositories {
mavenCentral()
} dependencies {
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.apache.commons:commons-lang3:3.8')
compile('com.auth0:java-jwt:3.4.0')
compile('com.alibaba:fastjson:1.2.47') testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
}

登录认证流程

Filter

对于用户登录行为,security通过定义一个Filter来拦截/login来实现的。spring security默认支持form方式登录,所以对于使用json发送登录信息的情况,我们自己定义一个Filter,这个Filter直接从AbstractAuthenticationProcessingFilter继承,只需要实现两部分,一个是RequestMatcher,指名拦截的Request类型;另外就是从json body中提取出username和password提交给AuthenticationManager。

public class MyUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public MyUsernamePasswordAuthenticationFilter() {
//拦截url为 "/login" 的POST请求
super(new AntPathRequestMatcher("/login", "POST"));
} @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
//从json中获取username和password
String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
String username = null, password = null;
if(StringUtils.hasText(body)) {
JSONObject jsonObj = JSON.parseObject(body);
username = jsonObj.getString("username");
password = jsonObj.getString("password");
} if (username == null)
username = "";
if (password == null)
password = "";
username = username.trim();
//封装到token中提交
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password); return this.getAuthenticationManager().authenticate(authRequest);
} }

Provider

前面的流程图中讲到了,封装后的token最终是交给provider来处理的。对于登录的provider,spring security已经提供了一个默认实现DaoAuthenticationProvider我们可以直接使用,这个类继承了AbstractUserDetailsAuthenticationProvider我们来看下关键部分的源代码是怎么做的。

public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
...
//这个方法返回true,说明支持该类型的token
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
... try {
// 获取系统中存储的用户信息
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
} } try {
//检查user是否已过期或者已锁定
preAuthenticationChecks.check(user);
//将获取到的用户信息和登录信息做比对
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
...
throw exception;
}
...
//如果认证通过,则封装一个AuthenticationInfo, 放到SecurityContext中
return createSuccessAuthentication(principalToReturn, authentication, user);
} ...
}

上面的代码中,核心流程就是retrieveUser()获取系统中存储的用户信息,再对用户信息做了过期和锁定等校验后交给additionalAuthenticationChecks()和用户提交的信息做比对。

这两个方法我们看他的继承类DaoAuthenticationProvider是怎么实现的。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* 加密密码比对
*/
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
} String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
/**
* 系统用户获取
*/
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
}

上面的方法实现中,用户获取是调用了UserDetailsService来完成的。这个是一个只有一个方法的接口,所以我们自己要做的,就是将自己的UserDetailsService实现类配置成一个Bean。下面是实例代码,真正的实现需要从数据库或者缓存中获取。

public class JwtUserService implements UserDetailsService{
//真实系统需要从数据库或缓存中获取,这里对密码做了加密
return User.builder().username("Jack").password(passwordEncoder.encode("jack-password")).roles("USER").build();
}

我们再来看另外一个密码比对的方法,也是委托给一个PasswordEncoder类来实现的。一般来说,存在数据库中的密码都是要经过加密处理的,这样万一数据库数据被拖走,也不会泄露密码。spring一如既往的提供了主流的加密方式,如MD5,SHA等。如果不显示指定的话,Spring会默认使用BCryptPasswordEncoder,这个是目前相对比较安全的加密方式。具体介绍可参考spring-security 的官方文档 - Password Endcoding

认证结果处理

filter将token交给provider做校验,校验的结果无非两种,成功或者失败。对于这两种结果,我们只需要实现两个Handler接口,set到Filter里面,Filter在收到Provider的处理结果后会回调这两个Handler的方法。

先来看成功的情况,针对jwt认证的业务场景,登录成功需要返回给客户端一个token。所以成功的handler的实现类中需要包含这个逻辑。

public class JsonLoginSuccessHandler implements AuthenticationSuccessHandler{

    private JwtUserService jwtUserService;

    public JsonLoginSuccessHandler(JwtUserService jwtUserService) {
this.jwtUserService = jwtUserService;
} @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//生成token,并把token加密相关信息缓存,具体请看实现类
String token = jwtUserService.saveUserLoginInfo((UserDetails)authentication.getPrincipal());
response.setHeader("Authorization", token);
} }

再来看失败的情况,登录失败比较简单,只需要回复一个401的Response即可。

public class HttpStatusLoginFailureHandler implements AuthenticationFailureHandler{
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
}

JsonLoginConfigurer

以上整个登录的流程的组件就完整了,我们只需要把它们组合到一起就可以了。这里继承一个AbstractHttpConfigurer,对Filter做配置。

public class JsonLoginConfigurer<T extends JsonLoginConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B>  {

    private MyUsernamePasswordAuthenticationFilter authFilter;

    public JsonLoginConfigurer() {
this.authFilter = new MyUsernamePasswordAuthenticationFilter();
} @Override
public void configure(B http) throws Exception {
//设置Filter使用的AuthenticationManager,这里取公共的即可
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//设置失败的Handler
authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
//不将认证后的context放入session
authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()); MyUsernamePasswordAuthenticationFilter filter = postProcess(authFilter);
//指定Filter的位置
http.addFilterAfter(filter, LogoutFilter.class);
}
//设置成功的Handler,这个handler定义成Bean,所以从外面set进来
public JsonLoginConfigurer<T,B> loginSuccessHandler(AuthenticationSuccessHandler authSuccessHandler){
authFilter.setAuthenticationSuccessHandler(authSuccessHandler);
return this;
} }

这样Filter就完整的配置好了,当调用configure方法时,这个filter就会加入security FilterChain的指定位置。这个是在全局定义的地方,我们放在最后说。在全局配置的地方,也会将DaoAuthenticationProvider放到ProviderManager中,这样filter中提交的token就可以被处理了。

带Token请求校验流程

用户除登录之外的请求,都要求必须携带JWT Token。所以我们需要另外一个Filter对这些请求做一个拦截。这个拦截器主要是提取header中的token,跟登录一样,提交给AuthenticationManager做检查。

Filter

public class JwtAuthenticationFilter extends OncePerRequestFilter{
...
public JwtAuthenticationFilter() {
//拦截header中带Authorization的请求
this.requiresAuthenticationRequestMatcher = new RequestHeaderRequestMatcher("Authorization");
} protected String getJwtToken(HttpServletRequest request) {
String authInfo = request.getHeader("Authorization");
return StringUtils.removeStart(authInfo, "Bearer ");
} @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//header没带token的,直接放过,因为部分url匿名用户也可以访问
//如果需要不支持匿名用户的请求没带token,这里放过也没问题,因为SecurityContext中没有认证信息,后面会被权限控制模块拦截
if (!requiresAuthentication(request, response)) {
filterChain.doFilter(request, response);
return;
}
Authentication authResult = null;
AuthenticationException failed = null;
try {
//从头中获取token并封装后提交给AuthenticationManager
String token = getJwtToken(request);
if(StringUtils.isNotBlank(token)) {
JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));
authResult = this.getAuthenticationManager().authenticate(authToken);
} else { //如果token长度为0
failed = new InsufficientAuthenticationException("JWT is Empty");
}
} catch(JWTDecodeException e) {
logger.error("JWT format error", e);
failed = new InsufficientAuthenticationException("JWT format error", 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) { //token认证成功
successfulAuthentication(request, response, filterChain, authResult);
} else if(!permissiveRequest(request)){
//token认证失败,并且这个request不在例外列表里,才会返回错误
unsuccessfulAuthentication(request, response, failed);
return;
}
filterChain.doFilter(request, response);
} ... protected boolean requiresAuthentication(HttpServletRequest request,
HttpServletResponse response) {
return requiresAuthenticationRequestMatcher.matches(request);
} protected boolean permissiveRequest(HttpServletRequest request) {
if(permissiveRequestMatchers == null)
return false;
for(RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
if(permissiveMatcher.matches(request))
return true;
}
return false;
}
}

这个Filter的实现跟登录的Filter有几点区别:

  • 经过这个Filter的请求,会继续过FilterChain中的其它Filter。因为跟登录请求不一样,token只是为了识别用户。
  • 如果header中没有认证信息或者认证失败,还会判断请求的url是否强制认证的(通过permissiveRequest方法判断)。如果请求不是强制认证,也会放过,这种情况比如博客类应用匿名用户访问查看页面;比如登出操作,如果未登录用户点击登出,我们一般是不会报错的。

    其它逻辑跟登录一样,组装一个token提交给AuthenticationManager

JwtAuthenticationProvider

同样我们需要一个provider来接收jwt的token,在收到token请求后,会从数据库或者缓存中取出salt,对token做验证,代码如下:

public class JwtAuthenticationProvider implements AuthenticationProvider{

    private JwtUserService userService;

    public JwtAuthenticationProvider(JwtUserService userService) {
this.userService = userService;
} @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
DecodedJWT jwt = ((JwtAuthenticationToken)authentication).getToken();
if(jwt.getExpiresAt().before(Calendar.getInstance().getTime()))
throw new NonceExpiredException("Token expires");
String username = jwt.getSubject();
UserDetails user = userService.getUserLoginInfo(username);
if(user == null || user.getPassword()==null)
throw new NonceExpiredException("Token expires");
String encryptSalt = user.getPassword();
try {
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(username)
.build();
verifier.verify(jwt.getToken());
} catch (Exception e) {
throw new BadCredentialsException("JWT token verify fail", e);
}
//成功后返回认证信息,filter会将认证信息放入SecurityContext
JwtAuthenticationToken token = new JwtAuthenticationToken(user, jwt, user.getAuthorities());
return token;
} @Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
} }

认证结果Handler

如果token认证失败,并且不在permissive列表中话,就会调用FailHandler,这个Handler和登录行为一致,所以都使用HttpStatusLoginFailureHandler 返回401错误。

token认证成功,在继续FilterChain中的其它Filter之前,我们先检查一下token是否需要刷新,刷新成功后会将新token放入header中。所以,新增一个JwtRefreshSuccessHandler来处理token认证成功的情况。

public class JwtRefreshSuccessHandler implements AuthenticationSuccessHandler{

    private static final int tokenRefreshInterval = 300;  //刷新间隔5分钟

    private JwtUserService jwtUserService;

    public JwtRefreshSuccessHandler(JwtUserService jwtUserService) {
this.jwtUserService = jwtUserService;
} @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
DecodedJWT jwt = ((JwtAuthenticationToken)authentication).getToken();
boolean shouldRefresh = shouldTokenRefresh(jwt.getIssuedAt());
if(shouldRefresh) {
String newToken = jwtUserService.saveUserLoginInfo((UserDetails)authentication.getPrincipal());
response.setHeader("Authorization", newToken);
}
} protected boolean shouldTokenRefresh(Date issueAt){
LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
return LocalDateTime.now().minusSeconds(tokenRefreshInterval).isAfter(issueTime);
} }

JwtLoginConfigurer

跟登录逻辑一样,我们定义一个configurer,用来初始化和配置JWTFilter。

public class JwtLoginConfigurer<T extends JwtLoginConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {

    private JwtAuthenticationFilter authFilter;

    public JwtLoginConfigurer() {
this.authFilter = new JwtAuthenticationFilter();
} @Override
public void configure(B http) throws Exception {
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
//将filter放到logoutFilter之前
JwtAuthenticationFilter filter = postProcess(authFilter);
http.addFilterBefore(filter, LogoutFilter.class);
}
//设置匿名用户可访问url
public JwtLoginConfigurer<T, B> permissiveRequestUrls(String ... urls){
authFilter.setPermissiveUrl(urls);
return this;
} public JwtLoginConfigurer<T, B> tokenValidSuccessHandler(AuthenticationSuccessHandler successHandler){
authFilter.setAuthenticationSuccessHandler(successHandler);
return this;
} }

配置集成

整个登录和无状态用户认证的流程都已经讲完了,现在我们需要吧spring security集成到我们的web项目中去。spring security和spring mvc做了很好的集成,一共只需要做两件事,给web配置类加上@EanbleWebSecurity,继承WebSecurityConfigurerAdapter定义个性化配置。

配置类WebSecurityConfig

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/image/**").permitAll() //静态资源访问无需认证
.antMatchers("/admin/**").hasAnyRole("ADMIN") //admin开头的请求,需要admin权限
.antMatchers("/article/**").hasRole("USER") //需登陆才能访问的url
.anyRequest().authenticated() //默认其它的请求都需要认证,这里一定要添加
.and()
.csrf().disable() //CRSF禁用,因为不使用session
.sessionManagement().disable() //禁用session
.formLogin().disable() //禁用form登录
.cors() //支持跨域
.and() //添加header设置,支持跨域和ajax请求
.headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(
new Header("Access-control-Allow-Origin","*"),
new Header("Access-Control-Expose-Headers","Authorization"))))
.and() //拦截OPTIONS请求,直接返回header
.addFilterAfter(new OptionRequestFilter(), CorsFilter.class)
//添加登录filter
.apply(new JsonLoginConfigurer<>()).loginSuccessHandler(jsonLoginSuccessHandler())
.and()
//添加token的filter
.apply(new JwtLoginConfigurer<>()).tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissiveRequestUrls("/logout")
.and()
//使用默认的logoutFilter
.logout()
// .logoutUrl("/logout") //默认就是"/logout"
.addLogoutHandler(tokenClearLogoutHandler()) //logout时清除token
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()) //logout成功后返回200
.and()
.sessionManagement().disable();
}
//配置provider
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider()).authenticationProvider(jwtAuthenticationProvider());
} @Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
} @Bean("jwtAuthenticationProvider")
protected AuthenticationProvider jwtAuthenticationProvider() {
return new JwtAuthenticationProvider(jwtUserService());
} @Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception{
//这里会默认使用BCryptPasswordEncoder比对加密后的密码,注意要跟createUser时保持一致
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
daoProvider.setUserDetailsService(userDetailsService());
return daoProvider;
}
...
}

以上的配置类主要关注一下几个点:

  • 访问权限配置,使用url匹配是放过还是需要角色和认证
  • 跨域支持,这个我们下面再讲
  • 禁用csrf,csrf攻击是针对使用session的情况,这里是不需要的,关于CSRF可参考 Cross Site Request Forgery
  • 禁用默认的form登录支持
  • logout支持,spring security已经默认支持logout filter,会拦截/logout请求,交给logoutHandler处理,同时在logout成功后调用LogoutSuccessHandler。对于logout,我们需要清除保存的token salt信息,这样再拿logout之前的token访问就会失败。请参考TokenClearLogoutHandler:
public class TokenClearLogoutHandler implements LogoutHandler {

    private JwtUserService jwtUserService;

    public TokenClearLogoutHandler(JwtUserService jwtUserService) {
this.jwtUserService = jwtUserService;
} @Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
clearToken(authentication);
} protected void clearToken(Authentication authentication) {
if(authentication == null)
return;
UserDetails user = (UserDetails)authentication.getPrincipal();
if(user!=null && user.getUsername()!=null)
jwtUserService.deleteUserLoginInfo(user.getUsername());
} }

角色配置

Spring Security对于访问权限的检查主要是通过AbstractSecurityIntercepter来实现,进入这个拦截器的基础一定是在context有有效的Authentication。

回顾下上面实现的UserDetailsService,在登录或token认证时返回的Authentication包含了GrantedAuthority的列表。

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//调用roles("USER")会将USER角色加入GrantedAuthority
return User.builder().username("Jack").password(passwordEncoder.encode("jack-password")).roles("USER").build();
}

然后我们上面的配置类中有对url的role做了配置。比如下面的配置表示/admin开头的url支持有admin和manager权限的用户访问:

.antMatchers("/admin/**").hasAnyRole("ADMIN,MANAGER")

对于Intecepter来说只需要吧配置中的信息和GrantedAuthority的信息一起提交给AccessDecisionManager来做比对。

跨域支持

前后端分离的项目需要支持跨域请求,需要做下面的配置。

CORS配置

首先需要在HttpSecurity配置中启用cors支持

http.cors()

这样spring security就会从CorsConfigurationSource中取跨域配置,所以我们需要定义一个Bean:

@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("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

Header配置

对于返回给浏览器的Response的Header也需要添加跨域配置:

http..headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(
//支持所有源的访问
new Header("Access-control-Allow-Origin","*"),
//使ajax请求能够取到header中的jwt token信息
new Header("Access-Control-Expose-Headers","Authorization"))))

OPTIONS请求配置

对于ajax的跨域请求,浏览器在发送真实请求之前,会向服务端发送OPTIONS请求,看服务端是否支持。对于options请求我们只需要返回header,不需要再进其它的filter,所以我们加了一个OptionsRequestFilter,填充header后就直接返回:

public class OptionsRequestFilter extends OncePerRequestFilter{

    @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if(request.getMethod().equals("OPTIONS")) {
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
response.setHeader("Access-Control-Allow-Headers", response.getHeader("Access-Control-Request-Headers"));
return;
}
filterChain.doFilter(request, response);
} }

总结

Spring Security在和shiro使用了类似的认证核心设计的情况下,提供了更多的和web的整合,以及更丰富的第三方认证支持。同时在安全性方面,也提供了足够多的默认支持,对得上security这个名字。

所以这两个框架的选择问题就相对简单了:

1)如果系统中本来使用了spring,那优先选择spring security;

2)如果是web系统,spring security提供了更多的安全性支持

3)除次之外可以选择shiro

文章内使用的源码已经放在git上:Spring Security and JWT demo

[参考资料]

Spring Security Reference

【转载】 Spring Security做JWT认证和授权的更多相关文章

  1. Springboot集成Spring Security实现JWT认证

    我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 简介 Spring Security作为成熟且强大的安全框架,得到许多大厂的青睐.而作为前后端分离的SSO方案,JWT ...

  2. Springboot WebFlux集成Spring Security实现JWT认证

    我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 简介 在之前的文章<Springboot集成Spring Security实现JWT认证>讲解了如何在传统 ...

  3. Spring Security和JWT实现登录授权认证

     目标 1.Token鉴权 2.Restful API 3.Spring Security+JWT 开始 自行新建Spring Boot工程 引入相关依赖 <dependency> < ...

  4. spring security oauth2 jwt 认证和资源分离的配置文件(java类配置版)

    最近再学习spring security oauth2.下载了官方的例子sparklr2和tonr2进行学习.但是例子里包含的东西太多,不知道最简单最主要的配置有哪些.所以决定自己尝试搭建简单版本的例 ...

  5. Spring Security(1):认证和授权的核心组件介绍及源码分析

    Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方式的安全框架.它包括认证(Authentication)和授权(Authorization)两个部 ...

  6. spring security 3 自定义认证,授权示例

    1,建一个web project,并导入所有需要的lib. 2,配置web.xml,使用Spring的机制装载: <?xml version="1.0" encoding=& ...

  7. Spring Security + JJWT 实现 JWT 认证和授权

    关于 JJWT 的使用,可以参考之前的文章:JJWT 使用示例 一.鉴权过滤器 @Component public class JwtAuthenticationTokenFilter extends ...

  8. Spring Security OAuth2.0认证授权五:用户信息扩展到jwt

    历史文章 Spring Security OAuth2.0认证授权一:框架搭建和认证测试 Spring Security OAuth2.0认证授权二:搭建资源服务 Spring Security OA ...

  9. 【Spring Cloud & Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权

    一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证授权.鉴权的逻辑,结合 ...

  10. Spring Security OAuth2.0认证授权三:使用JWT令牌

    Spring Security OAuth2.0系列文章: Spring Security OAuth2.0认证授权一:框架搭建和认证测试 Spring Security OAuth2.0认证授权二: ...

随机推荐

  1. 精彩回顾|【ACDU 中国行·杭州站】数据库主题交流活动成功举办!

    8月19日下午,[ACDU 中国行·杭州站]在杭州西溪万怡酒店圆满落下帷幕.本次活动由中国数据库联盟(ACDU)联合墨天轮社区主办,蚂蚁集团 OceanBase 及亚信科技 AntDB 赞助支持.六位 ...

  2. Data Summit 2022 大会资料分享(共23个)

    Data Summit (数据峰会)是国际上关于数据管理.分析方面最新战略和技术研讨的顶尖峰会,通过行业领先的企业.专家的分享,共同探讨大数据和数据科学领域的发展现状与最新技术研究. 2022年5月1 ...

  3. go~wasm插件的开发

    Go和TinyGo是两种不同的Go语言编译器,它们之间有以下几点区别: 目标平台: Go:Go语言编译器主要面向通用计算机平台,如Windows.Linux.macOS等. TinyGo:TinyGo ...

  4. 5.29 相约杭州!云原生 Meetup 第二期杭州站开启报名

    以容器技术和容器编排为基础的云原生应用,被越来越多的企业用户接受和使用,并且在生产环境中使用容器技术的比例逐年增加.KubeSphere 作为一款面向应用的开源容器混合云,经过 3 年的发展和 10 ...

  5. OpenFunction v1.1.0 发布:新增 v1beta2 API,支持 Dapr 状态管理

    OpenFunction 是一个开源的云原生 FaaS(Function as a Service,函数即服务)平台,旨在帮助开发者专注于业务逻辑的研发.在过去的几个月里,OpenFunction 社 ...

  6. C++版的Mc

    非常垃圾的c++版Mc. #include<bits/stdc++.h> #include<windows.h> #include<conio.h> using n ...

  7. 详解 JuiceFS 在多云架构下的数据同步与一致性

    随着大模型流行,GPU 算力资源正变得日益稀缺,传统的"算力跟着存储跑"的策略需要转变为"存储跟着算力跑".为了确保数据一致性和管理的便捷性,企业通常在特定地区 ...

  8. 快速部署和测试ingress-nginx:1.9.6

    点击查看代码 下载deploy文件 https://github.com/kubernetes/ingress-nginx/blob/controller-v1.9.6/deploy/static/p ...

  9. AI五子棋_05 公钥加解密 10进制转256进制

    AI 五子棋 第五步 恭喜你到达第五步! 我想你一定很艰难,前一步的问题需要大数运算,因为这个算法依赖于质因数分解的复杂度,只有数字相当大时才能保证这个算法难于破解. 这是服务器使用的公钥: 6553 ...

  10. 指针进阶(函数指针)(C语言)

    目录 1. 字符指针变量 2. 数组指针变量 2.1 数组指针变量是什么? 2.2 数组指针变量怎么初始化? 3. 二维数组传参的本质 4. 函数指针变量 4.1 函数指针变量的创建 4.2 函数指针 ...