简介

参考自定义认证逻辑中的Authentication 类

  1. public interface Authentication extends Principal, Serializable {
  2. Collection<? extends GrantedAuthority> getAuthorities();
  3. Object getCredentials();
  4. Object getDetails();
  5. Object getPrincipal();
  6. boolean isAuthenticated();
  7. void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
  8. }

其中有一个方法叫做 getDetails。该方法实际上就是用来存储有关身份认证的其他信息的,例如 IP 地址、证书信息
在默认情况下,这里存储的就是用户登录的 IP 地址和 sessionId。
那么我们就可通过该方法获取到相关的IP地址信息

用户登录必经的一个过滤器就是 UsernamePasswordAuthenticationFilter,在该类的 attemptAuthentication 方法中,对请求参数做提取,在 attemptAuthentication 方法中,会调用到一个方法,就是 setDetails。

protected void setDetails(HttpServletRequest request,
  UsernamePasswordAuthenticationToken authRequest) {
 authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
image.png

UsernamePasswordAuthenticationToken 是 Authentication 的具体实现,
所以这里实际上就是在设置 details,
至于 details 的值,则是通过 authenticationDetailsSource 来构建的

WebAuthenticationDetailsSource

public class WebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    public WebAuthenticationDetailsSource() {
    }

    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new WebAuthenticationDetails(context);
    }
}

WebAuthenticationDetails

public class WebAuthenticationDetails implements Serializable {
    private static final long serialVersionUID = 550L;
    private final String remoteAddress;
    private final String sessionId;

    public WebAuthenticationDetails(HttpServletRequest request) {
        this.remoteAddress = request.getRemoteAddr();
        HttpSession session = request.getSession(false);
        this.sessionId = session != null ? session.getId() : null;
    }

    private WebAuthenticationDetails(String remoteAddress, String sessionId) {
        this.remoteAddress = remoteAddress;
        this.sessionId = sessionId;
    }
    ......

}

默认通过 WebAuthenticationDetailsSource 来构建 WebAuthenticationDetails,并将结果设置到 Authentication 的 details 属性中去。而 WebAuthenticationDetails 中定义的属性,大家看一下基本上就明白,这就是保存了用户登录地址和 sessionId。
用户登录的 IP 地址实际上我们可以直接从 WebAuthenticationDetails 中获取到。

则当我们登陆成功后,我们可以在程序的任何地方获取到相对应的details

@Service
public class HelloService {
    public void hello() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
        System.out.println(details);
    }
}

定制

当然,WebAuthenticationDetails 也可以自己定制,
因为默认它只提供了 IP 和 sessionid 两个信息,如果我们想保存关于 Http 请求的更多信息,就可以通过自定义 WebAuthenticationDetails 来实现。

例如验证码验证也可以放在自定义的WebAuthenticationDetails中进行操作
我们定义如下两个类:

MyWebAuthenticationDetails

public class MyWebAuthenticationDetails extends WebAuthenticationDetails {
    private static final long serialVersionUID = 603157756847194200L;
    private boolean isCaptcha;

    public MyWebAuthenticationDetails(HttpServletRequest req) {
        super(req);
        String code = req.getParameter("code");
        String verify_code = (String) req.getSession().getAttribute("verify_code");
        if(code != null && code.equals(verify_code)) {
            isCaptcha = true;
        }
    }

    public boolean isPassed() {
        return isCaptcha;
    }
}

MyWebAuthenticationDetailsSource

@Component
public class MyWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
    @Override
    public MyWebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new MyWebAuthenticationDetails(context);
    }
}

执行逻辑如下:

  • 首先我们定义 MyWebAuthenticationDetails,由于它的构造方法中,刚好就提供了 HttpServletRequest 对象,所以我们可以直接利用该对象进行验证码判断,并将判断结果交给 isPassed 变量保存。
  • 「如果我们想扩展属性,只需要在 MyWebAuthenticationDetails 中再去定义更多属性,然后从 HttpServletRequest 中提取出来设置给对应的属性即可,这样,在登录成功后就可以随时随地获取这些属性了。」
  • 最后在 MyWebAuthenticationDetailsSource 中构造并返回。

接下来,我们就可以直接在 MyAuthenticationProvider 中进行调用了:

MyAuthenticationProvider

public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (!((MyWebAuthenticationDetails) authentication.getDetails()).isCaptcha()) {
            throw new AuthenticationServiceException("验证码错误");
        }
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

直接从 authentication 中获取到 details 并调用 isCaptcha方法
有问题就抛出异常即可。

配置使用自定义

最后的问题就是如何用自定义的 MyWebAuthenticationDetailsSource 代替系统默认的 WebAuthenticationDetailsSource,很简单,我们只需要在 SecurityConfig 中稍作定义即可:


    @Autowired
    MyWebAuthenticationDetailsSource myWebAuthenticationDetailsSource;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/captcha")
            .permitAll()
            .antMatchers("/rememberme/**")
            .rememberMe()//该接口只有使用了remember登录才能访问
            .antMatchers("/admin/**")
            .fullyAuthenticated()//fullyAuthenticated 不同于 authenticated,fullyAuthenticated 不包含自动登录的形式,而
            .anyRequest()
            .authenticated()
            .and() //所有请求都需要认证才能访问
            .formLogin()
            .authenticationDetailsSource(myWebAuthenticationDetailsSource)
            .permitAll()
            .successHandler((req, resp, auth) -> {//auth: 当前登录成功的用户信息
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter writer = resp.getWriter();
                writer.write(new ObjectMapper().writeValueAsString(auth.getPrincipal()));
                writer.flush();
                writer.close();
            })
            .failureHandler((req, resp, exception) -> {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter writer = resp.getWriter();
                writer.write(new ObjectMapper().writeValueAsString(exception.getMessage()));
                writer.flush();
                writer.close();
            })        
            .and()
            .rememberMe()
            .key("zukxu")
            .tokenRepository(jdbcTokenRepository())
            .and()//添加记住我功能
            .csrf().disable()//关闭csrf
        ;
    }

.formLogin()
.authenticationDetailsSource(myWebAuthenticationDetailsSource)

将 MyWebAuthenticationDetailsSource 注入到 SecurityConfig 中,并在 formLogin 中配置 authenticationDetailsSource 即可成功使用我们自定义的 WebAuthenticationDetails。

这样自定义完成后,WebAuthenticationDetails 中原有的功能依然保留,也就是我们还可以利用老办法继续获取用户 IP 以及 sessionId 等信息,如下:

@Service
public class HelloService {
    public void hello() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();
        System.out.println(details);
    }
}

这里类型强转的时候,转为 MyWebAuthenticationDetails 即可。

测试

image.png
MyWebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=D54876F951918A08852C1803FE26DA20]