Security在现阶段的开发中使用频率非常高,用于权限认证;项目前后端分离的认证,前端登录也不仅仅是单一的账号密码登录,常常会有验证码登录以及各种第三方登录等等,下面我们就一一讲解Security的实现方式。
源码地址:戳我查看
1. 认证流程
先列出几个关键文件
- UsernamePasswordAuthenticationFilter
- UsernamePasswordAuthenticationToken
- AuthenticationManager(ProviderManager)
- AuthenticationProvider
Security整个认证流程可以理解为对过滤器以及认证管理的CRUD,第一步是添加过滤器,第二部添加令牌对象,第三步添加认证处理器,最后添加到配置中就结束了。
UsernamePasswordAuthenticationFilter
这是Security默认的过滤器,继承AbstractAuthenticationProcessingFilter,看个源码片段
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
这里过滤器做了两件事,添加过滤路径,从request中获取表单传入的用户名及密码生成令牌对象传入认证管理器。
UsernamePasswordAuthenticationToken
没什么好说的看源码就行,接收用户名密码封装对象,这里可以添加自定义数据。
AuthenticationManager
ProviderManager的源码中有这样一段代码:
private List<AuthenticationProvider> providers = Collections.emptyList();
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
...
这里for循环遍历了所有的认证器,调用每个认证器的supports方法来判断是否执行下面的authenticate认证方法。
AuthenticationProvider
这是一个接口,里面只有两个方法:
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
authenticate是用于实现认证的,supports用于判断是否执行该认证器的,只有当supports返回true的时候才会执行。
2. 前后端分离认证
前面讲了认证流程,这里我们就走一遍这个流程,单认证器实现前后端分离认证。
2.1 实现过滤器
public class CustomAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static final String AUTH_TYPE = "authType";
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final String OAUTH_TOKEN_URL = "/home/login";
private static final String HTTP_METHOD_POST = "POST";
public CustomAuthenticationProcessingFilter() {
super(new AntPathRequestMatcher(OAUTH_TOKEN_URL, HTTP_METHOD_POST));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (!HTTP_METHOD_POST.equals(request.getMethod().toUpperCase())) {
throw new AuthenticationServiceException("不支持的请求方式: " + request.getMethod());
}
AbstractAuthenticationToken token = new JwtAuthenticatioToken(
request.getParameter(USERNAME), request.getParameter(PASSWORD), request.getParameter(AUTH_TYPE)
);
this.setDetails(request, token);
return this.getAuthenticationManager().authenticate(token);
}
protected void setDetails(HttpServletRequest request, AbstractAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
2.2 自定义令牌对象
这里前后端都使用这个认证器,在令牌中添加authType字段,便于认证器判断。
public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
private String authType;
public JwtAuthenticatioToken(Object principal, Object credentials) {
super(principal, credentials);
}
public JwtAuthenticatioToken(Object principal, Object credentials, String authType) {
super(principal, credentials);
this.authType = authType;
}
public JwtAuthenticatioToken(Object principal, Object credentials, String authType, String token) {
super(principal, credentials);
this.token = token;
this.authType = authType;
}
public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
super(principal, credentials, authorities);
this.token = token;
}
public String getAuthType() {
return authType;
}
public void setAuthType(String authType) {
this.authType = authType;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
}
2.3 实现认证器
public class AdminAuthenticationProvider implements AuthenticationProvider {
@Autowired
private PcUserDao pcUserDao;
@Autowired
private BackUserDao backUserDao;
@Override
public Authentication authenticate(Authentication authentication) {
if (authentication.getPrincipal() == null) {
throw new RuntimeException("用户名为空");
}
JwtAuthenticatioToken token = (JwtAuthenticatioToken) authentication;
String authType = token.getAuthType();
if ("PC".equals(authType)) {
PcUserEntity byPhone = pcUserDao.findByPhone((String) token.getPrincipal());
if (byPhone == null) {
throw new UsernameNotFoundException("用户不存在");
}
} else if ("BACK".equals(authType)) {
BackUserEntity byPhone = backUserDao.findByPhone((String) token.getPrincipal());
if (byPhone == null) {
throw new UsernameNotFoundException("用户不存在");
}
} else {
// 其它方式
}
return new JwtAuthenticatioToken(authentication.getPrincipal(), authentication.getCredentials(), JwtTokenUtils.generateToken(authentication));
}
@Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticatioToken.class.isAssignableFrom(authentication));
}
}
3. 多种认证方式
上面只是使用了单一的认证器判断自定义字段调用原始的Security账号密码认证逻辑认证;当我们有自己的登录方式时这就满足不了了,下面我们举例说说验证码该如何来进行验证。
只需要对过滤器以及认证器的CRUD即可。
3.1 自定义过滤器
public class MobileAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static final String USERNAME = "mobile";
private static final String PASSWORD = "code";
private static final String OAUTH_TOKEN_URL = "/mobile/login";
private static final String HTTP_METHOD_POST = "POST";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(OAUTH_TOKEN_URL,
HTTP_METHOD_POST);
private boolean postOnly = true;
public MobileAuthenticationProcessingFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public MobileAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = request.getParameter(USERNAME);
username = (username != null) ? username : "";
username = username.trim();
String password = request.getParameter(PASSWORD);
password = (password != null) ? password : "";
AbstractAuthenticationToken authRequest = new MobileAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected void setDetails(HttpServletRequest request, AbstractAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
3.2 自定义令牌
public class MobileAuthenticationToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public MobileAuthenticationToken(Object principal, Object credentials) {
super(principal, credentials);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public MobileAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
// 不用返回密码
super(principal, credentials, authorities);
}
public MobileAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
super(principal, credentials, authorities);
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
3.3 验证码登录认证器
这里我们可以注入自定义的service,Security默认的UserDetailsService中有一个loadUserByUsername方法用于判断用户状态等。
@Component
public class MobileAuthenticationProvider implements AuthenticationProvider {
/**
* 注入自定义的service
*/
@Autowired
private MobileUserDetailService mobileUserDetailService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
JwtUserDetails mobileDetails = (JwtUserDetails) mobileUserDetailService.loadUserByUsername(username);
String cachePwd = mobileDetails.getPassword();
// 比较表单输入的验证码
if (!cachePwd.equals(password)) {
throw new BadCredentialsException("验证码错误");
}
/*try {
// 登录认证等
} catch (Exception e) {
e.printStackTrace();
throw new BadCredentialsException(e.getMessage());
}*/
MobileAuthenticationToken token = new MobileAuthenticationToken(username, password, mobileDetails.getAuthorities(), JwtTokenUtils.generateToken(authentication));
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}
@Override
public boolean supports(Class<?> authentication) {
/**
* providerManager会遍历所有securityconfig中注册的provider集合
* 根据此方法返回true或false来决定由哪个provider
* 去校验请求过来的authentication
*/
return (MobileAuthenticationToken.class.isAssignableFrom(authentication));
}
}
在自定义的service中我们可以校验验证码,设置用户权限等。
@Service
public class MobileUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = "";
Boolean flag = false;
List<GrantedAuthority> authorities = new ArrayList<>();
// 根据用户名查询数据库中的用户,判断是否存在;获取缓存中的code等
// 用户权限列表
List<String> permissions = new ArrayList<>();
permissions.add("insert");
permissions.add("update");
permissions.add("remove");
permissions.add("delete");
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
// 缓存中的验证码
password = "1234";
// 用户账号状态
flag = true;
authorities = grantedAuthorities;
return new JwtUserDetails(username, password, flag, authorities);
}
}
4. 完整的配置文件
自定义登录成功以及登录失败的文件看本项目的源码吧
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CusAuthenticationSuccessHandler successHandler;
@Autowired
private CusAuthenticationFailureHandler failureHandler;
@Autowired
private MobileAuthenticationProvider mobileAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置认证方式等
auth.authenticationProvider(adminAuthenticationProvider());
auth.authenticationProvider(mobileAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//http相关的配置,包括登入登出、异常处理、会话管理等
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/mobile/login", "/home/login").permitAll()
.anyRequest().authenticated();
//各类错误异常处理 以下针对于访问资源路径 认证异常捕获 和 无权限处理
http.exceptionHandling().authenticationEntryPoint((req, resp, exception) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
//封装异常描述信息
String json = JSONObject.toJSONString(exception.getMessage());
out.write(json);
out.flush();
out.close();
}).accessDeniedHandler((resq, resp, exception) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
String json = JSONObject.toJSONString("无权限:");
out.write(json);
out.flush();
out.close();
});
http.addFilterAfter(externalAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
}
/**
* 方式一
*
* @return
*/
@Bean
public CustomAuthenticationProcessingFilter externalAuthenticationProcessingFilter() {
CustomAuthenticationProcessingFilter filter = new CustomAuthenticationProcessingFilter();
filter.setAuthenticationManager(authenticationManager);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationFailureHandler(failureHandler);
return filter;
}
/**
* 方式二
*
* @return
*/
@Bean
public MobileAuthenticationProcessingFilter mobileAuthenticationProcessingFilter() {
MobileAuthenticationProcessingFilter mobileFilter = new MobileAuthenticationProcessingFilter(authenticationManager);
mobileFilter.setAuthenticationSuccessHandler(successHandler);
mobileFilter.setAuthenticationFailureHandler(failureHandler);
return mobileFilter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public AdminAuthenticationProvider adminAuthenticationProvider() {
AdminAuthenticationProvider adminAuthenticationProvider = new AdminAuthenticationProvider();
return adminAuthenticationProvider;
}
/**
* 新版本的security规定必须设置一个默认的加密方式
* 用于登录验证 注册等
*
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}