概念
認(rèn)證:即登錄瘾带,authentication
授權(quán):即允許某種操作鼠哥,authorization
會話:即保持已登錄狀態(tài)
RBAC:Role-Based Access Control ,基于角色的訪問控制
業(yè)務(wù)系統(tǒng):即前臺用戶系統(tǒng)
內(nèi)管系統(tǒng):即后臺管理系統(tǒng)
Spring Boot Security 應(yīng)用組成
1看政、初始化Spring Boot 應(yīng)用
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>
加了依賴管理后允蚣,設(shè)置依賴不需要指明版本于颖,且不需要以下配置:
<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厉萝、從數(shù)據(jù)庫中查詢用戶詳情的實現(xiàn)類
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根據(jù)用戶名恍飘,從數(shù)據(jù)庫找出用戶信息
// 參數(shù)依次為:用戶名榨崩,數(shù)據(jù)庫里記錄的密碼,可用章母,未過期母蛛,密碼未過期乳怎,未被鎖定,權(quán)限列表
// Spring Security 會 自動調(diào)用 PasswordEncoder.match() 來判斷密碼是否正確
return new org.springframework.security.core.userdetails.User(username, passwordEncoder.encode("密碼"), true, true,
true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_角色1,權(quán)限1"));
}
}
5秫逝、登錄成功 违帆、 登錄失敗刷后、登出成功尝胆、賬號被踢出含衔、無訪問權(quán)限 處理類
@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 里有權(quán)限列表
}
}
@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 認(rèn)證失敗的原因
}
}
@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 默認(rèn)提供的登錄頁面
.loginProcessingUrl("/api/login") // 指定 登錄接口 url
.successHandler(loginSuccessHandler) // 指定登錄成功處理類寺渗,不指定則重定向
.failureHandler(loginFailureHandler) // 指定登錄失敗處理類信殊,不指定則重定向
.and()
.authorizeRequests() // 開始授權(quán)配置
.antMatchers("/*.html").permitAll() // 對*.html 的請求涡拘,無需權(quán)限
.antMatchers(HttpMethod.POST, "/manage/*").hasRole("manager") // 對 /manage/* 的請求据德,需要擁有manager角色
.antMatchers("/client/*").hasAuthority("client") // 對 /client/* 的請求,需要擁有client權(quán)限
.anyRequest().authenticated() // 針對所有請求橱野,進行身份認(rèn)證
.and()
.logout() // 開始 登出配置
.logoutUrl("/signOut") // 登出接口水援,默認(rèn)為 /logout
.logoutSuccessUrl("/login.html") // 登出重定向到的路徑蜗元,默認(rèn)為loginPage
.logoutSuccessHandler(logOutHandler) // 與logoutSuccessUrl互斥
.deleteCookies("JSESSION") // 登出時 清理 cookie
.and()
.csrf() // 開始csrf配置
.disable() // 放開csrf防御
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); // 無權(quán)限訪問的處理
// 只配置 loginPage 時奕扣,可能出現(xiàn) “無訪問權(quán)限時 瀏覽器彈出一個默認(rèn)登錄框”惯豆,為規(guī)避這種情況循帐,可以配置authenticationEntryPoint()
http.sessionManagement().maximumSessions(1) // 同一賬號只允許一處登錄
.maxSessionsPreventsLogin(false) // 允許后登錄者踢出先登錄者
.expiredSessionStrategy(expiredSessionStrategy); // 被踢出時的請求響應(yīng)
super.configure(http);
}
@Bean
public PasswordEncoder passwordEncoder() {
// 這是Spring提供的一個密碼加密器拄养,加鹽散列瘪匿,并將鹽拼入散列值棋弥,可用防止散列撞庫
return new BCryptPasswordEncoder(); // 也可以自己實現(xiàn)一個 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唧垦、認(rèn)證異常類
// 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(); // 也可以自己實現(xiàn)一個 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 實現(xiàn) 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數(shù)據(jù)模型
表:用戶表嫂便,角色表闸与,資源(權(quán)限)表,用戶角色關(guān)系表厂画,角色資源關(guān)系表
資源表:存儲權(quán)限控制目標(biāo)袱院,例如:菜單忽洛、按鈕环肘、URL
最佳實踐
1、業(yè)務(wù)系統(tǒng)苍在,一般權(quán)限控制比較簡單,無需RBAC
2、內(nèi)管系統(tǒng)续誉,需要RBAC酷鸦,并且系統(tǒng)中有管理RBAC數(shù)據(jù)的界面
3、資源表的值可以設(shè)置為 "對象.操作"嘹裂,例如 "order.delete"表示訂單的刪除權(quán)限摔握,"order.delete"表示訂單的刪除權(quán)限,"coupon.all"表示優(yōu)惠券的所有權(quán)限
4伊磺、前端 根據(jù) RBAC 數(shù)據(jù) 隱藏 入口(菜單删咱,按鈕)
5、后端 根據(jù) RBAC 數(shù)據(jù)表存儲的 角色 與 可訪問的 URL 控制訪問
Spring Security 整合 RBAC
1摘能、RBAC 權(quán)限判斷 類
// 案例 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();
// 根據(jù)用戶名 找到 當(dāng)前用戶可訪問的 url 列表
Set<String> urls = new HashSet<String>();
for(String url : urls){
if(antPathMatcher.match(url, request.getRequestURI())){
return true;
}
}
}
return false;
}
}
// 案例 2
// 權(quán)限的格式為 module.method团搞,判斷當(dāng)前url是否包含在權(quán)限列表里
@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,默認(rèn)為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")) {
// 權(quán)限列表里有的模塊仑扑,才允許訪問镇饮,且請求方法需要匹配
if (method.equals("get") || method.equals(allow) || allow.equals("all")) {
return true;
}
}
}
return false;
}
}
2储藐、Spring Security 配置
http.authorizeRequests()
.antMatchers("/*.html").permitAll() // 對*.html 的請求嘶是,放開所有權(quán)限
// 進行權(quán)限判斷聂喇,此外仍然會判斷是否認(rèn)真
.antMatchers("/manage").access("@rbacService.hasPermission(request, authentication)") // 放在authenticated()前面
.anyRequest().authenticated() // anyRequest()必須放在authorizeRequests的最后,且只能有一個
super.configure(http);