概念
認證:即登錄,authentication
授權:即允許某種操作,authorization
會話:即保持已登錄狀態
RBAC:Role-Based Access Control ,基于角色的訪問控制
業務系統:即前臺用戶系統
內管系統:即后臺管理系統
Spring Boot Security 應用組成
1、初始化Spring Boot 應用
2、在pom中增加依賴管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId> <!-- Spring Framework依賴管理,來自Spring IO Platform項目 -->
<version>Cairo-SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId> <!-- Spring Cloud 依賴管理 -->
<version>Greenwich.M3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
加了依賴管理后,設置依賴不需要指明版本,且不需要以下配置:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
</parent>
3、配置Maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
4、從數據庫中查詢用戶詳情的實現類
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根據用戶名,從數據庫找出用戶信息
// 參數依次為:用戶名,數據庫里記錄的密碼,可用,未過期,密碼未過期,未被鎖定,權限列表
// Spring Security 會 自動調用 PasswordEncoder.match() 來判斷密碼是否正確
return new org.springframework.security.core.userdetails.User(username, passwordEncoder.encode("密碼"), true, true,
true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_角色1,權限1"));
}
}
5、登錄成功 、 登錄失敗、登出成功、賬號被踢出、無訪問權限 處理類
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
// 返回一個 json
response.getWriter().write(objectMapper.writeValueAsString(authentication)); // authentication 里有權限列表
}
}
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
// 返回一個 json
response.getWriter().write(objectMapper.writeValueAsString(e)); // e 認證失敗的原因
}
}
@Component
public class LogOutHandler implements LogoutSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
// 返回一個 json
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
@Component
public class ExpiredSessionStrategy implements SessionInformationExpiredStrategy {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
event.getResponse().setContentType("application/json;charset=UTF-8");
RestfulResult result = new RestfulResult(-1, "您的賬號已在別處登錄");
event.getResponse().getWriter().write(objectMapper.writeValueAsString(result));
}
}
@Component
public class AuthenticationEntryPoint implements org.springframework.security.web.AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
response.setStatus(HttpStatus.FOUND.value());
response.setHeader("location", "https://**.com/web/sign_out");
}
}
6、Spring Security 配置類
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private LogOutHandler logOutHandler;
@Autowired
private ExpiredSessionStrategy expiredSessionStrategy;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 使用表單登錄
.loginPage("/login.html") // 未登錄時重定向到登錄頁面, 不指定則使用Spring security 默認提供的登錄頁面
.loginProcessingUrl("/api/login") // 指定 登錄接口 url
.successHandler(loginSuccessHandler) // 指定登錄成功處理類,不指定則重定向
.failureHandler(loginFailureHandler) // 指定登錄失敗處理類,不指定則重定向
.and()
.authorizeRequests() // 開始授權配置
.antMatchers("/*.html").permitAll() // 對*.html 的請求,無需權限
.antMatchers(HttpMethod.POST, "/manage/*").hasRole("manager") // 對 /manage/* 的請求,需要擁有manager角色
.antMatchers("/client/*").hasAuthority("client") // 對 /client/* 的請求,需要擁有client權限
.anyRequest().authenticated() // 針對所有請求,進行身份認證
.and()
.logout() // 開始 登出配置
.logoutUrl("/signOut") // 登出接口,默認為 /logout
.logoutSuccessUrl("/login.html") // 登出重定向到的路徑,默認為loginPage
.logoutSuccessHandler(logOutHandler) // 與logoutSuccessUrl互斥
.deleteCookies("JSESSION") // 登出時 清理 cookie
.and()
.csrf() // 開始csrf配置
.disable() // 放開csrf防御
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); // 無權限訪問的處理
// 只配置 loginPage 時,可能出現 “無訪問權限時 瀏覽器彈出一個默認登錄框”,為規避這種情況,可以配置authenticationEntryPoint()
http.sessionManagement().maximumSessions(1) // 同一賬號只允許一處登錄
.maxSessionsPreventsLogin(false) // 允許后登錄者踢出先登錄者
.expiredSessionStrategy(expiredSessionStrategy); // 被踢出時的請求響應
super.configure(http);
}
@Bean
public PasswordEncoder passwordEncoder() {
// 這是Spring提供的一個密碼加密器,加鹽散列,并將鹽拼入散列值,可用防止散列撞庫
return new BCryptPasswordEncoder(); // 也可以自己實現一個 PasswordEncoder
}
@Bean
// 允許跨域配置
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin(CorsConfiguration.ALL); // 或 config.setAllowedOriginPatterns(Collections.singletonList("*"));
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS"); // AllowedMethod 必須羅列,而不能用通配符 *
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
圖片驗證碼驗證
1、配置Maven依賴
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
2、認證異常類
// AuthenticationException 是抽象類,不能實例化,因此需要自定義一個 驗證碼異常類
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
3、驗證碼過濾器
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
private LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 只過濾登錄接口
if (StringUtils.equals("/api/login", request.getRequestURI()) && StringUtils.equalsIgnoreCase("post", request.getMethod())) {
try {
validate(new ServletWebRequest(request));
}
catch (ValidateCodeException e) {
loginFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 這里從session中取出驗證碼值進行比對
throw new ValidateCodeException("驗證碼不匹配");
}
}
4、Spring Security配置類
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ValidateCodeFilter validateCodeFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 在驗證賬號 之前 驗證 圖片驗證碼
http.addFilterBefore( validateCodeFilter, UsernamePasswordAuthenticationFilter.class )
.formLogin();
super.configure(http);
}
@Bean
public PasswordEncoder passwordEncoder() {
// 這是Spring提供的一個密碼加密器,加鹽散列,并將鹽拼入散列值,可用防止散列撞庫
return new BCryptPasswordEncoder(); // 也可以自己實現一個 PasswordEncoder
}
}
短信登錄
1、仿照 UsernamePasswordAuthenticationToken,定義Token類
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
2、仿照 UsernamePasswordAuthenticationFilter,定義Filter類
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile"; // 字段名
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/api/mobileLogin", "POST")); // 短信登錄接口
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String parameter) {
this.mobileParameter = parameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
3、仿照 DaoAuthenticationProvider 實現 Provider類
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
4、短信驗證碼過濾器
// 與圖片驗證碼過濾器 類似
5、配置 Filter類 和 Provider類
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
@Autowired
private MyUserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
6、Spring Security 配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private ValidateCodeFilter validateCodeFilter;
@Autowired
private SmsCodeAuthenticationSecurityConfig authenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore( validateCodeFilter, UsernamePasswordAuthenticationFilter.class )
.formLogin()
.apply(authenticationSecurityConfig);
super.configure(http);
}
}
禁用 Spring Security
注解啟動類 ServletInitializer
@EnableAutoConfiguration(exclude = {
SecurityAutoConfiguration.class
})
RBAC數據模型
表:用戶表,角色表,資源(權限)表,用戶角色關系表,角色資源關系表
資源表:存儲權限控制目標,例如:菜單、按鈕、URL
最佳實踐
1、業務系統,一般權限控制比較簡單,無需RBAC
2、內管系統,需要RBAC,并且系統中有管理RBAC數據的界面
3、資源表的值可以設置為 "對象.操作",例如 "order.delete"表示訂單的刪除權限,"order.delete"表示訂單的刪除權限,"coupon.all"表示優惠券的所有權限
4、前端 根據 RBAC 數據 隱藏 入口(菜單,按鈕)
5、后端 根據 RBAC 數據表存儲的 角色 與 可訪問的 URL 控制訪問
Spring Security 整合 RBAC
1、RBAC 權限判斷 類
// 案例 1
@Component("rbacService")
public class RbacService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
if(principal instanceof UserDetails){
// 用戶名
String username = ((UserDetails)principal).getUsername();
// 根據用戶名 找到 當前用戶可訪問的 url 列表
Set<String> urls = new HashSet<String>();
for(String url : urls){
if(antPathMatcher.match(url, request.getRequestURI())){
return true;
}
}
}
return false;
}
}
// 案例 2
// 權限的格式為 module.method,判斷當前url是否包含在權限列表里
@Component("rbacService")
public class RbacService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 未登錄
if(authentication.getPrincipal() instanceof String){
return false;
}
Collection<? extends GrantedAuthority> authorityList = authentication.getAuthorities();
String method = request.getMethod().toLowerCase();
String path = request.getServletPath();
for (GrantedAuthority authority : authorityList) {
// authority 的格式為 module.allow
// allow取值: get、post、put、delete、all,默認為all
String[] authoritySegment = authority.getAuthority().split("\\.");
String module = authoritySegment[0];
String allow = "all";
if(authoritySegment.length > 1){
allow = authoritySegment[1];
}
String regexStart = "/admin/" + module + "/";
String regexEnd = "/admin/" + module;
if (path.startsWith(regexStart) || path.endsWith(regexEnd) || module.equals("all")) {
// 權限列表里有的模塊,才允許訪問,且請求方法需要匹配
if (method.equals("get") || method.equals(allow) || allow.equals("all")) {
return true;
}
}
}
return false;
}
}
2、Spring Security 配置
http.authorizeRequests()
.antMatchers("/*.html").permitAll() // 對*.html 的請求,放開所有權限
// 進行權限判斷,此外仍然會判斷是否認真
.antMatchers("/manage").access("@rbacService.hasPermission(request, authentication)") // 放在authenticated()前面
.anyRequest().authenticated() // anyRequest()必須放在authorizeRequests的最后,且只能有一個
super.configure(http);