前後端分離 SpringBoot整合SpringSecurity權限控制(動態攔截url)
Spring Security是一個功能強大且可高度自定義的身份驗證和訪問控制框架。它是保護基於Spring的應用程序的事實上的標準。
Spring Security是一個專注於爲Java應用程序提供身份驗證和授權的框架。與所有Spring項目一樣,Spring Security的真正強大之處在於它可以輕鬆擴展以滿足自定義要求。
本篇博客詳細記錄了
SpringBoot整合SpringSecurity前後端分離項目的後端代碼細節,並將完整項目上傳至gitHub(更改數據源配置即可運行)。
項目完整功能:
- 登錄(根據所登錄用戶所具有的權限返回給前端JSON數據,動態的展示功能菜單)。
- 註銷。
- 動態攔截url請求,根據當前登錄用戶所具有的權限,進行權限認證(防止不具有權限的用戶直接通過url請求功能)。
- 用戶管理模塊(查詢所有用戶[分頁加模糊查詢]、新增用戶、修改用戶、刪除用戶、爲用戶分配角色)。
- 角色管理模塊(查詢所有角色[分頁加模糊查詢]、新增角色、修改角色、刪除角色、爲角色分配可訪問的資源菜單)。
- 菜單管理模塊(查詢所有菜單、新增菜單、修改菜單、刪除菜單)。
- 登錄驗證中增加額外數據(如ip地址,mac地址,驗證碼等)。
GitHub: link. 歡迎star
友情鏈接:SpringBoot整合SpringSecurity+JWT實現單點登錄
maven依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.登錄攔截全局配置
@Configuration
public class WebSecurityConfigure extends WebSecurityConfigurerAdapter {
@Resource
private UrlAuthenticationEntryPoint authenticationEntryPoint; //自定義未登錄時:返回狀態碼401
@Resource
private UrlAuthenticationSuccessHandler authenticationSuccessHandler; //自定義登錄成功處理器:返回狀態碼200
@Resource
private UrlAuthenticationFailureHandler authenticationFailureHandler; //自定義登錄失敗處理器:返回狀態碼402
@Resource
private UrlAccessDeniedHandler accessDeniedHandler; //自定義權限不足處理器:返回狀態碼403
@Resource
private UrlLogoutSuccessHandler logoutSuccessHandler; //自定義註銷成功處理器:返回狀態碼200
@Resource
private SelfAuthenticationProvider authenticationProvider; //自定義登錄認證
@Resource
private SelfFilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource; //動態獲取url權限配置
@Resource
private SelfAccessDecisionManager accessDecisionManager; //自定義權限判斷管理器
@Resource
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource; //身份驗證詳細信息源
@Override
public void configure(WebSecurity web) {
// web.ignoring().antMatchers("/connect/**"); //無條件允許訪問
web.ignoring().antMatchers("/common/**"); //無條件允許訪問
}
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider); //自定義登錄認證
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 關閉csrf驗證(防止跨站請求僞造攻擊)
http.csrf().disable();
// 未登錄時:返回狀態碼401
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
// 無權訪問時:返回狀態碼403
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
// url權限認證處理
http.antMatcher("/**").authorizeRequests()
// .antMatchers("/security/user/**").hasRole("ADMIN") //需要ADMIN角色纔可以訪問
// .antMatchers("/connect").hasIpAddress("127.0.0.1") //只有ip[127.0.0.1]可以訪問'/connect'接口
.anyRequest() //其他任何請求
.authenticated() //都需要身份認證
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource); //動態獲取url權限配置
o.setAccessDecisionManager(accessDecisionManager); //權限判斷
return o;
}
});
// 將session策略設置爲無狀態的,通過token進行權限認證
// http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 開啓自動配置的登錄功能
http.formLogin() //開啓登錄
// .loginPage("/login") //登錄頁面(前後端不分離)
.loginProcessingUrl("/nonceLogin") //自定義登錄請求路徑(post)
.usernameParameter("username").passwordParameter("password") //自定義登錄用戶名密碼屬性名,默認爲username和password
// .successForwardUrl("/index") //登錄成功後的url(post,前後端不分離)
// .failureForwardUrl("/error") //登錄失敗後的url(post,前後端不分離)
.successHandler(authenticationSuccessHandler) //驗證成功處理器(前後端分離):返回狀態碼200
.failureHandler(authenticationFailureHandler) //驗證失敗處理器(前後端分離):返回狀態碼402
.authenticationDetailsSource(authenticationDetailsSource); //身份驗證詳細信息源(登錄驗證中增加額外字段)
// 開啓自動配置的註銷功能
http.logout() //用戶註銷, 清空session
.logoutUrl("/nonceLogout") //自定義註銷請求路徑
// .logoutSuccessUrl("/bye") //註銷成功後的url(前後端不分離)
.logoutSuccessHandler(logoutSuccessHandler); //註銷成功處理器(前後端分離):返回狀態碼200
}
}
2.(1)自定義登錄認證
@Slf4j
@Component
public class SelfAuthenticationProvider implements AuthenticationProvider {
@Resource
private SelfUserDetailsService selfUserDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
log.info("authentication >> {}", JSONObject.toJSONString(authentication, SerializerFeature.WriteMapNullValue));
CustomWebAuthenticationDetails customWebAuthenticationDetails = (CustomWebAuthenticationDetails) authentication.getDetails(); //獲取身份驗證詳細信息
// String remoteAddress = customWebAuthenticationDetails.getRemoteAddress();
// String sessionId = customWebAuthenticationDetails.getSessionId();
// System.out.println("remoteAddress >> " + remoteAddress);
// System.out.println("sessionId >> " + sessionId);
// System.out.println("details >> " + JSONObject.toJSONString(customWebAuthenticationDetails, SerializerFeature.WriteMapNullValue));
System.out.println("macAddress >> " + customWebAuthenticationDetails.getMacAddress()); //用於校驗mac地址白名單(這裏只是打個比方,登錄驗證中增加的額外字段)
String username = (String) authentication.getPrincipal(); //表單輸入的用戶名
String password = (String) authentication.getCredentials(); //表單輸入的密碼
UserDetails userInfo = selfUserDetailsService.loadUserByUsername(username);
boolean matches = new BCryptPasswordEncoder().matches(password, userInfo.getPassword()); //校驗用戶名密碼
if (!matches) {
throw new BadCredentialsException("The password is incorrect!!");
}
return new UsernamePasswordAuthenticationToken(username, userInfo.getPassword(), userInfo.getAuthorities());
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
2.(2)自定義用戶認證
@Component
public class SelfUserDetailsService implements UserDetailsService {
@Resource
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SelfUserDetails userInfo = new SelfUserDetails();
userInfo.setUsername(username); //任意登錄用戶名
String password = userService.findPasswordByUsernameAfterValidTime(username);
if (ObjectUtils.isEmpty(password)) {
throw new UsernameNotFoundException("User name" + username + "not find!!");
}
userInfo.setPassword(password); //從數據庫獲取密碼
Set<SimpleGrantedAuthority> authoritiesSet = new HashSet<>();
List<String> roles = userService.findRoleNameByUsername(username);
for (String roleName : roles) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleName); //用戶擁有的角色
authoritiesSet.add(simpleGrantedAuthority);
}
userInfo.setAuthorities(authoritiesSet);
return userInfo;
}
}
2.(3)自定義web身份驗證詳細信息(用於登錄驗證中增加額外參數)
class CustomWebAuthenticationDetails extends WebAuthenticationDetails implements Serializable {
private String macAddress;
CustomWebAuthenticationDetails(HttpServletRequest httpServletRequest) {
super(httpServletRequest);
// Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
// while (headerNames.hasMoreElements()) {
// String s = headerNames.nextElement();
// String header = httpServletRequest.getHeader(s);
// System.out.println(s + ": " + header);
// }
macAddress = httpServletRequest.getParameter("macAddress");
}
String getMacAddress() {
return macAddress;
}
}
2.(4)自定義身份驗證詳細信息源
@Component
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest httpServletRequest) {
return new CustomWebAuthenticationDetails(httpServletRequest);
}
}
3.動態獲取url權限配置
@Slf4j
@Component
public class SelfFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Resource
private UserService userService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
Set<ConfigAttribute> set = new HashSet<>();
// 獲取請求地址
String requestUrl = ((FilterInvocation) o).getRequestUrl();
log.info("requestUrl >> {}", requestUrl);
List<String> menuUrl = userService.findAllMenuUrl();
for (String url : menuUrl) {
if (antPathMatcher.match(url, requestUrl)) {
List<String> roleNames = userService.findRoleNameByMenuUrl(url); //當前請求需要的權限
roleNames.forEach(roleName -> {
SecurityConfig securityConfig = new SecurityConfig(roleName);
set.add(securityConfig);
});
}
}
if (ObjectUtils.isEmpty(set)) {
return SecurityConfig.createList("ROLE_LOGIN");
}
return set;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
4.自定義權限判斷管理器
@Slf4j
@Component
public class SelfAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
// 當前請求需要的權限
log.info("collection:{}", collection);
// 當前用戶所具有的權限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
log.info("principal:{} authorities:{}", authentication.getPrincipal().toString(), authorities);
for (ConfigAttribute configAttribute : collection) {
// 當前請求需要的權限
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("Not logged in!!");
} else {
return;
}
}
// 當前用戶所具有的權限
for (GrantedAuthority grantedAuthority : authorities) {
// 包含其中一個角色即可訪問
if (grantedAuthority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("SimpleGrantedAuthority!!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
5.自定義未登錄時:返回狀態碼401
@Component
public class UrlAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
UrlResponse response = new UrlResponse();
response.setSuccess(false);
response.setCode("401");
response.setMessage(e.getMessage());
response.setData(null);
httpServletResponse.setStatus(401);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.getWriter().write(GsonUtil.GSON.toJson(response));
}
}
6.自定義登錄成功處理器:返回狀態碼200
@Component
public class UrlAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
UrlResponse response = new UrlResponse();
response.setSuccess(true);
response.setCode("200");
response.setMessage("Login Success!");
String username = (String) authentication.getPrincipal(); //表單輸入的用戶名
String password = (String) authentication.getCredentials(); //表單輸入的密碼
Map<String, Object> userInfo = userService.findMenuInfoByUsername(username, response);
response.setData(userInfo);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.getWriter().write(GsonUtil.GSON.toJson(response));
}
}
7.自定義登錄失敗處理器:返回狀態碼402
@SuppressWarnings("Duplicates")
@Component
public class UrlAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
UrlResponse response = new UrlResponse();
response.setSuccess(false);
response.setCode("402");
response.setMessage(e.getMessage());
response.setData(null);
httpServletResponse.setStatus(402);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.getWriter().write(GsonUtil.GSON.toJson(response));
}
}
8.自定義權限不足處理器:返回狀態碼403
@Component
public class UrlAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
UrlResponse response = new UrlResponse();
response.setSuccess(false);
response.setCode("403");
response.setMessage(e.getMessage());
response.setData(null);
httpServletResponse.setStatus(403);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.getWriter().write(GsonUtil.GSON.toJson(response));
}
}
9.自定義註銷成功處理器:返回狀態碼200
@Component
public class UrlLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
UrlResponse response = new UrlResponse();
response.setSuccess(true);
response.setCode("200");
response.setMessage("Logout Success!!");
response.setData(null);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.getWriter().write(GsonUtil.GSON.toJson(response));
}
}
10.application.yml
server:
port: 8018
servlet:
session:
timeout: 6h
spring:
# main:
# web-application-type: none
# security:
# user:
# name: security_admin #設置默認登錄用戶名
# password: security_admin #設置默認登錄密碼 不設置會在控制檯打印出,默認用戶名是user
datasource:
url: ######:3306/block?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2b8&autoReconnect=true&failOverReadOnly=false
username: ######
password: ######
driver-class-name: com.mysql.jdbc.Driver
hikari:
read-only: false
connection-timeout: 60000
idle-timeout: 60000
validation-timeout: 3000
max-lifetime: 60000
login-timeout: 5
maximum-pool-size: 60
minimum-idle: 10
jpa:
generate-ddl: false
show-sql: false
hibernate:
ddl-auto: none
database: mysql
open-in-view: true
12.數據庫表結構設計
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for authority_user
-- ----------------------------
DROP TABLE IF EXISTS `authority_user`;
CREATE TABLE `authority_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`username` varchar(255) DEFAULT NULL COMMENT '用戶名',
`password` varchar(255) DEFAULT NULL COMMENT '密碼',
`email` varchar(255) DEFAULT NULL COMMENT '郵箱',
`phone` varchar(255) DEFAULT NULL COMMENT '手機號',
`valid_time` varchar(255) DEFAULT NULL COMMENT '有效截止時間',
`update_time` varchar(255) DEFAULT NULL COMMENT '更新時間',
`remark` mediumtext COMMENT '備註',
`nickname` varchar(255) DEFAULT NULL COMMENT '暱稱',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for authority_role
-- ----------------------------
DROP TABLE IF EXISTS `authority_role`;
CREATE TABLE `authority_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`role_name` varchar(255) DEFAULT NULL COMMENT '角色名稱(必須以ROLE_起始命名)',
`role_name_CN` varchar(255) DEFAULT NULL COMMENT '角色名稱中文',
`update_time` varchar(255) DEFAULT NULL COMMENT '更新時間',
`remark` varchar(255) DEFAULT NULL COMMENT '備註',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for authority_menu
-- ----------------------------
DROP TABLE IF EXISTS `authority_menu`;
CREATE TABLE `authority_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`url` varchar(255) DEFAULT NULL COMMENT '請求路徑',
`menu_name` varchar(255) DEFAULT NULL COMMENT '菜單名稱',
`parent_id` int(11) DEFAULT NULL COMMENT '父菜單id',
`update_time` varchar(255) DEFAULT NULL COMMENT '更新時間',
`remark` varchar(255) DEFAULT NULL COMMENT '備註',
`url_pre` varchar(255) DEFAULT NULL COMMENT '路由(前端自己匹配用)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=73 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for authority_user_role
-- ----------------------------
DROP TABLE IF EXISTS `authority_user_role`;
CREATE TABLE `authority_user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` int(11) DEFAULT NULL,
`role_id` int(11) DEFAULT NULL,
`update_time` varchar(255) DEFAULT NULL COMMENT '更新時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=63 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for authority_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `authority_role_menu`;
CREATE TABLE `authority_role_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`role_id` int(11) DEFAULT NULL,
`menu_id` int(11) DEFAULT NULL,
`update_time` varchar(255) DEFAULT NULL COMMENT '更新時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=798 DEFAULT CHARSET=utf8;
GitHub: link. 歡迎star