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. Csharp的CancellationToken 案例

    using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using Syst ...

  2. C# 根据主键ID查询数据库的数据 反射和泛型实现

    // 引入命名空间 using Zhu.ADO.NET.DBProxy; using Zhu.ADO.NET.Models.models; Console.WriteLine("====== ...

  3. 让查询可以使用 json path

    记录一下最近sv.db的完善 1. 让查询可以使用 json path 有时候我们会存储 json 到 db,也有时会只取json部分数据,或者通过json部分数据进行过滤 所以sv.db 也支持这些 ...

  4. KubeSphere Cloud 月刊|灾备支持 K8s 1.22+,轻量集群支持安装灾备和巡检组件

    功能升级 备份容灾服务支持 K8s v1.22+ 版本集群 随着 Kubernetes 近一年频繁的发版.升级,越来越多的用户开始部署并使用高版本的 Kubernetes 集群.备份容灾服务支持 Ku ...

  5. Vulnhub 靶机 THE PLANETS: EARTH

    0x01信息收集 1.1.nmap扫描 IP段扫描,确定靶机地址 平扫描 nmap 192.168.1.0/24 扫描结果(部分) Nmap scan report for earth.local ( ...

  6. dirseach目录扫描工具-安装详细教程

    安装: 1.github源码下载解压 使用 git 安装: 推荐git clone https://github.com/maurosoria/dirsearch.git --depth 1 zip文 ...

  7. RAC环境中某数据文件(非system表空间)创建在本地,不停机迁移到ASM磁盘中

    Datafiles are mistakenly built into the local file system for processing in the RAC environment The ...

  8. Flink RetractStream示例及UDF函数实现

    介绍 今天在Flink 1.7.2版本上跑一个Flink SQL 示例 RetractPvUvSQL,报 Exception in thread "main" org.apache ...

  9. re中文匹配

    Pattern = re.compile(u'[\u4e00-\u9fa5]+') if Pattern.search(searchstring): # do something else: # do ...

  10. 《Django 5 By Example》阅读笔记:p237-p338

    <Django 5 By Example>学习第11天,p237-p338总结,总计102页. 一.技术总结 1.follow system(关注功能) 表之间的关系有三种:OneToOn ...