主要就講解一下,如何使用Security實現token認證,順帶也講講默認的登錄,是如何實現的。
(helloworld級別的樣例代碼,可運行,仍然需要思考如何應用於實戰)
登錄流程
簡單的登錄流程如下:filter攔截登錄請求;provider驗證密碼以及其它信息;驗證成功走success回調,失敗走failure回調。
登錄成功之后的操作:
1、如果是token認證,成功之后需要寫回token,之后客戶端的每一個請求,都需要攜帶token,此外,還需要一個獨立的filter,攔截所有的請求,判斷token是不是有效的。
2、如果是session,那就往session中存儲用戶信息。
(Provider 集中了大部分的鑒權邏輯,默認實現可以看:DaoAuthenticationProvider)
AuthenticationToken(登錄令牌)
使用 Security 登錄時,需要將用戶信息封裝成 Authentication,Authentication 包含了登錄所需的關鍵參數,整個認證流程都會有 Authentication 的參與。
1、UsernamePasswordAuthenticationToken 是 Authentication的子類,源碼中這類名稱很多,實際是一個東西;
2、這個對象包含了用戶的賬號、密碼,以及其它登錄所需的的信息;
3、這個對象是有狀態變化的,“未認證的” 和 “已經完成認證的”(實際上說的是 setAuthenticated(false) 函數,詳見 UsernamePasswordAuthenticationToken 源碼);
4、設計Authentication類的時候,盡量避免使用繼承的寫法(Provider 是根據 Authentication 的 class進行區分的,寫完要監測注意執行效果)。
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 直接復制{@link UsernamePasswordAuthenticationToken}全部源碼,根據自己的需求進行代碼擴展;
* 注意{@link UsernamePasswordAuthenticationToken}的構造函數,2個參數的和3個參數的,效果是不一樣的;
* 因為每一個Token都有對應的Provider,最好避免采用繼承的方式寫Token。
*
* @author Mr.css
* @date 2021-12-23 10:51
*/
public class AuthenticationToken extends AbstractAuthenticationToken {
//這里省略全部代碼
}
AbstractAuthenticationProcessingFilter(鑒權處理攔截)
執行的優先級非常高,即使沒有配置 Sevlet 或者 Controller,代碼也可以執行。
代碼執行結束,需要返回已經認證完畢 Authentication,如果認證成功,繼續走成功的回調接口,如果認證失敗,就走失敗的接口。
1、想看 Security 默認的功能實現,可以參考 UsernamePasswordAuthenticationFilter 代碼;
2、主要功能:攔截登錄請求,發起認證,最終返回鑒權結束的 Authentication;
3、代碼不會自動調用 Provider,需要手動執行 super.getAuthenticationManager().authenticate(authentication) 函數。
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 登錄驗證
*
* @author Mr.css
* @date 2021-12-23 11:41
*/
public class LoginFilter extends AbstractAuthenticationProcessingFilter {
LoginFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
/**
* 嘗試認證,獲取request中的數據,發起認證
*
* @param request -
* @param response -
* @return returning a fully populated Authentication object (including granted authorities)
* @throws AuthenticationException -
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String userName = request.getParameter("userName");
String pwd = request.getParameter("pwd");
System.out.println("結果過濾器攔截...");
AuthenticationToken authentication = new AuthenticationToken(userName, pwd);
//發起認證,經過程序流轉,最終會到達Provider
return super.getAuthenticationManager().authenticate(authentication);
}
}
AuthenticationProvider
Provider 包含兩個主要功能,一個是查詢,一個就是認證,找到用戶的詳細信息,然后證明用戶的賬號、密碼都是有效的。
這個類包含兩個函數:
supports(Class<?> authentication):用於說明當前的 Provider 可以解析哪些 Authentication;
authenticate(Authentication authentication):認證用戶信息,參數與返回值一致,完成鑒權結束,要調整 Authentication 的狀態。
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
/**
* 用戶認證
*
* @author Mr.css
* @date 2021-12-23 10:53
*/
public class AuthenticationProvider extends DaoAuthenticationProvider {
/**
* 標明當前Provider能夠處理的Token類型
*
* @param authentication tokenClass
* @return boolean
*/
@Override
public boolean supports(Class<?> authentication) {
return AuthenticationToken.class == authentication;
}
/**
* 身份鑒權
*
* @param authentication 身份證明
* @return Authentication 已經完成的身份證明(a fully authenticated object including credentials)
* @throws AuthenticationException e
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
System.out.println("身份認證:" + authentication);
return authentication;
}
}
UserDetailsService
相當於DAO,主要就是負責用戶身份信息查詢,包括密碼、權限,下面代碼是生產環境直接扣出來的,提供參考代碼,按需調整。
import cn.seaboot.admin.user.bean.entity.Role;
import cn.seaboot.admin.user.bean.entity.User;
import cn.seaboot.admin.user.bean.entity.UserGroup;
import cn.seaboot.admin.user.service.PermService;
import cn.seaboot.admin.user.service.RoleService;
import cn.seaboot.admin.user.service.UserGroupService;
import cn.seaboot.admin.user.service.UserService;
import cn.seaboot.common.core.CommonUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* 查詢用戶詳細信息
*
* @author Mr.css
* @date 2020-05-08 0:02
*/
@Configuration
public class CustomUserDetailsService implements UserDetailsService {
@Resource
private PasswordHelper passWordHelper;
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private UserGroupService userGroupService;
@Resource
private PermService permService;
/**
* 因為security自身的設計原因,角色權限前面需要添加ROLE前綴
*/
private static final String ROLE_PREFIX = "ROLE_";
/**
* 默認添加一個權限,名稱為登錄,標明必須登錄才能訪問(個性化設計:只是為了方便組織代碼邏輯)
*/
private static final String ROLE_LOGIN = "ROLE_LOGIN";
/**
* 因為security自身的設計原因,我們在用戶分組和角色權限,增加ROLE前綴
*
* @param role 角色
* @return SimpleGrantedAuthority
*/
private SimpleGrantedAuthority genSimpleGrantedAuthority(String role) {
if (!role.startsWith(ROLE_PREFIX)) {
role = ROLE_PREFIX + role;
}
return new SimpleGrantedAuthority(role);
}
/**
* 用戶登錄並賦予權限
*
* @param userName 用戶帳號
* @return UserDetails 用戶詳細信息
* @throws UsernameNotFoundException 拋出具體的異常
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
System.out.println("登錄的用戶是:" + userName);
User user = userService.queryByUserCode(userName);
UserGroup userGroup = userGroupService.queryById(user.getOrgId(), user.getGroupId());
Role sysRole = roleService.queryById(user.getOrgId(), userGroup.getRoleId());
//用戶權限列表
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//添加用戶組權限
if (CommonUtils.isNotEmpty(userGroup.getCode())) {
String role = userGroup.getCode();
grantedAuthorities.add(this.genSimpleGrantedAuthority(role));
}
//添加角色權限
if (CommonUtils.isNotEmpty(sysRole.getRoleCode())) {
String role = sysRole.getRoleCode();
grantedAuthorities.add(this.genSimpleGrantedAuthority(role));
}
//添加普通權限
Set<String> perms = permService.selectConcisePermsByRoleId(userGroup.getRoleId());
for (String perm : perms) {
if (CommonUtils.isNotEmpty(perm)) {
grantedAuthorities.add(new SimpleGrantedAuthority(perm));
}
}
// TODO: 測試時可以刪除上面其它權限配置,這里僅提供參考
grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_LOGIN));
// TODO:獲取BCrypt加密的密碼,按需調整,這里我用的是自己的加密算法,可以直接使用BCryptPasswordEncoder
String bCryptPassword = passWordHelper.getBCryptPassword(user.getPassword(), user.getPasswordSalt());
return new org.springframework.security.core.userdetails.User(user.getUserCode(), bCryptPassword, grantedAuthorities);
}
}
AuthenticationSuccessHandler
身份認證成功回調函數,如果普通登錄,就進行頁面轉發,如果是token認證,就向客戶端寫回一個token。
import cn.seaboot.admin.mvc.Result;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 身份認證成功
*
* @author Mr.css
* @date 2021-12-23 11:59
*/
public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
* 認證成功之后調用
*
* @param request -
* @param response -
* @param authentication 認證信息
* @throws IOException -from write
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setContentType("application/json;charset=UTF-8");
System.out.println("身份認證成功:" + authentication);
//TODO: 登錄成功,將token寫回客戶端
response.getWriter().write(JSON.toJSONString(Result.succeed()));
}
}
AuthenticationFailureHandler
在認證過程中,出現認證問題,需要拋出異常,在這里統一處理。
import cn.seaboot.admin.mvc.Result;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 登錄失敗異常處理
*
* @author Mr.css
* @date 2021-12-23 12:01
*/
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Result result;
if (exception instanceof BadCredentialsException ||
exception instanceof UsernameNotFoundException) {
result = Result.failed("賬戶名或者密碼輸入錯誤!");
} else if (exception instanceof LockedException) {
result = Result.failed("賬戶被鎖定,請聯系管理員!");
} else if (exception instanceof CredentialsExpiredException) {
result = Result.failed("密碼過期,請聯系管理員!");
} else if (exception instanceof DisabledException) {
result = Result.failed("賬戶被禁用,請聯系管理員!");
} else {
result = Result.failed("登陸失敗!");
}
response.getWriter().write(result.toString());
}
}
TokenFilter
前面這些操作,只是完成了登錄流程,代碼還沒有完全結束。
不管是 session,還是使用 token,登錄成功之后,都需要一個獨立的filter,攔截所有的請求,證明你已經登陸過了。
如果是 token 認證,就需要驗證,你的每一個請求是否包含了 token,並且需要驗證 token 是否還有效。
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 每一次請求都需要校驗一次token
*
* @author Mr.css
* @date 2021-12-23 15:14
*/
public class TokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("Authentication");
System.out.println("token" + token);
//授權
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_LOGIN"));
//正常設計,在LoginFilter那一步就必須創建Authentication,這里為了演示,創建一個虛擬的Authentication。
AuthenticationToken authenticationToken = new AuthenticationToken("admin", "test", grantedAuthorities);
authenticationToken.setDetails(new WebAuthenticationDetails(httpServletRequest));
//添加到上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//未登錄直接拋出異常,交給spring異常切面統一處理,也可以自定義其它處理方式
//throw new AccessDeniedException("登錄未授權!");
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
配置類
將上面這一堆代碼,組合起來,注冊進 spring 容器中
import cn.seaboot.admin.security.bean.entity.SecurityChain;
import cn.seaboot.common.core.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
import java.util.List;
/**
* Security configuration
*
* @author Mr.css
* @date 2020-05-07 23:38
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
@Resource
private CustomUserDetailsService customUserDetailsService;
@Resource
private BCryptPasswordEncoder passwordHelper;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* HttpSecurity相關配置
*
* @param http HttpSecurity
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//前面我們設置的登陸接口是/login,因此/login的配置是permitAll
registry.antMatchers("/login").access("permitAll");
// 添加攔截器
LoginFilter loginFilter = new LoginFilter();
TokenFilter tokenFilter = new TokenFilter();
loginFilter.setAuthenticationSuccessHandler(new LoginSuccessHandler());
loginFilter.setAuthenticationFailureHandler(new LoginFailureHandler());
loginFilter.setAuthenticationManager(super.authenticationManager());
http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(tokenFilter, LoginFilter.class);
//禁用CSRF,默認用於防止CSRF攻擊的設置,模版引擎中使用
http.csrf().disable();
// 基於token,所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//禁用掉XFrameOptions,這個配置能讓IFrame無法嵌套我們的頁面,可以防止盜鏈,
http.headers().frameOptions().disable();
}
/**
* 設置用戶登錄和密碼加密功能
*
* @param auth AuthenticationManagerBuilder
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) {
AuthenticationProvider authenticationProvider = new AuthenticationProvider();
authenticationProvider.setUserDetailsService(customUserDetailsService);
authenticationProvider.setPasswordEncoder(passwordHelper);
auth.authenticationProvider(authenticationProvider);
}
}