簡介
未更新此文檔爲spring boot2.2以下版本,因爲內容較多,整理更新
Spring Security是一個功能強大且可高度自定義的身份驗證和訪問控制框架。它是保護基於Spring的應用程序的事實標準。
Spring Security是一個專注於爲Java應用程序提供身份驗證和授權的框架。與所有Spring項目一樣,Spring Security的真正強大之處在於它可以輕鬆擴展以滿足自定義要求
特徵
-
對身份驗證和授權的全面和可擴展的支持
-
防止會話固定,點擊劫持,跨站點請求僞造等攻擊
-
Servlet API集成
- 可選與Spring MVC集成
主要介紹:基本登錄 ,openId登錄,oauth2登錄
maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
基於註解的認證
@EnableWebSecurity 開啓security
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
爲基於方法的安全註解
1.securedEnabled = true 一般不使用
開啓框架的原生@Secured
註釋,spring security底層註解
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
2.prePostEnabled = true 使用最多
開啓 @PreAuthorize 註解,先大體寫個示例
//表現層使用
@GetMapping("create")
@PreAuthorize("hasRole('role_admin')")//hasRole爲SpEL表達式
public SysUser create(SysUser sysUser) {
//...
}
//接口中使用(表現層也可以使用)
public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}
3.jsr250Enabled = true 一般不使用
由java提供JSR是Java Specification Requests的縮寫,意思是Java 規範提案。是指向JCP(Java Community Process)提出新增一個標準化技術規範的正式請求。詳情:鏈接
spring boot 配置security
自定義配置信息
@Getter
@Setter
@ConfigurationProperties(prefix = "project.security")//配置前綴
public class SecurityProperties {
// 免認證資源路徑
private String[] anonResourcesUrl;
//登錄地址
private String loginUrl;
//退出地址
private String logoutUrl;
//持久化登錄是否在啓動創建表
private boolean createTableOnStartup;
//超時時間默認30分鐘
private Duration sessionTimeout = Duration.ofMinutes(30);
//最大併發登錄數量,默認值爲-1,表示無限制
private int maximumSession;
}
application.yml中添加
#自定義配置
project:
security:
#免認證資源路徑
anon-resources-url: /public/**,/static/**
#登錄地址
login-url: /login
#退出地址
logout-url: /logout
#30分鐘
session-timeout: PT30M
#最大併發登錄數量
maximum-session: 1
#持久化登錄是否在啓動創建表
create-table-on-startup: false
SecurityConfig 配置
響應配置
public class AuthResponseIO extends HashMap<String, Object> {
//200 登錄成功
public static AuthResponseIO ok(String msg) {
AuthResponseIO AuthResponse = new AuthResponseIO();
AuthResponse.put("code", HttpStatus.OK.value());
AuthResponse.put("msg", msg);
return AuthResponse;
}
//401 登錄未成功
public static AuthResponseIO unAuthorized(String msg) {
AuthResponseIO AuthResponse = new AuthResponseIO();
AuthResponse.put("code", HttpStatus.UNAUTHORIZED.value());
AuthResponse.put("msg", msg);
return AuthResponse;
}
//403
public static AuthResponseIO forbidden() {
AuthResponseIO AuthResponse = new AuthResponseIO();
AuthResponse.put("code", HttpStatus.FORBIDDEN.value());
AuthResponse.put("msg", "無權限訪問!");
return AuthResponse;
}
//404
public static AuthResponseIO notFound() {
AuthResponseIO AuthResponse = new AuthResponseIO();
AuthResponse.put("code", HttpStatus.NOT_FOUND.value());
AuthResponse.put("msg", "請求未找到!");
return AuthResponse;
}
//500
public static AuthResponseIO error() {
AuthResponseIO AuthResponse = new AuthResponseIO();
AuthResponse.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
AuthResponse.put("msg", "服務器內部錯誤!");
return AuthResponse;
}
public static AuthResponseIO error(String msg) {
AuthResponseIO AuthResponse = new AuthResponseIO();
AuthResponse.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
AuthResponse.put("msg", msg);
return AuthResponse;
}
@Override
public AuthResponseIO put(String key, Object value) {
super.put(key, value);
return this;
}
}
Security headler配置
/**
* 登錄成功處理器
*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
WebAuthenticationDetails details = (WebAuthenticationDetails) authentication.getDetails();
//表示接收到身份驗證請求的TCP / IP地址
//RemoteAddress()無法僞造,因爲建立TCP連接需要三次握手.如果僞造了源 IP,無法建立TCP連接,
//更不會有後面的HTTP請求,如果是代理地址使用下面的getIP。
String remoteAddress = details.getRemoteAddress();
Object principal = authentication.getPrincipal();
if (principal instanceof MyUserDetails) {
MyUserDetails userDetails = (MyUserDetails) principal;
userDetails.setRemoteAddress(remoteAddress);
}
response.setContentType(ResponseContent.JSON_UTF8.getValue());
response.setStatus(HttpStatus.OK.value());
response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.ok("登錄成功!")));
}
}
/**
* 獲取IP地址
* 使用 Ngnix等反向代理軟件, 則不能通過request.getRemoteAddr()獲取IP地址
* 如果使用了多級反向代理的話,X-Forwarded-For的值並不止一個,而是一串IP地址,X-Forwarded-For中第一個非 unknown的有效IP字符串,則爲真實IP地址
*/
public static String getIP(HttpServletRequest request) {
if (Objects.nonNull(request)) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
ip = request.getHeader("Proxy-Client-IP");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
ip = request.getHeader("WL-Proxy-Client-IP");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
ip = request.getHeader("HTTP_CLIENT_IP");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
ip = request.getRemoteAddr();
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip))
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
log.error("未知主機異常{}", e.getMessage());
}
return ip;
}
return null;
}
/**
* 登錄失敗處理器
*/
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String message;
if (exception instanceof UsernameNotFoundException) {
message = "用戶不存在!";
} else if (exception instanceof BadCredentialsException) {
message = "密碼錯誤!";
} else if (exception instanceof LockedException) {
message = "用戶已被鎖定!";
} else if (exception instanceof DisabledException) {
message = "用戶不可用!";
} else if (exception instanceof AccountExpiredException) {
message = "賬戶已過期!";
} else if (exception instanceof CredentialsExpiredException) {
message = "用戶密碼已過期!";
} else {
message = "認證失敗,請聯繫網站管理員!";
}
response.setContentType(ResponseContent.JSON_UTF8.getValue());
response.sendError(HttpStatus.UNAUTHORIZED.value(), message);
response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.unAuthorized(message)));
}
}
/**
* 訪問被拒絕處理
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType(ResponseContent.JSON_UTF8.getValue());
response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.forbidden()));
response.sendError(HttpStatus.FORBIDDEN.value(),"無權限訪問!");
}
//不分離配置
// private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
//
// @Override
// public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// response.setContentType("application/json;charset=utf-8");
// response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.forbidden()));
// response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
// redirectStrategy.sendRedirect(request, response, "/403.html");
// }
}
/**
* 配置退出處理器,解決登出後 principals 中還存在相應的 sessionInformation 的問題
*/
@Component
public class MyLogoutHandler implements LogoutHandler {
@Autowired
private ApplicationContext applicationContext;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String sessionId = request.getRequestedSessionId();
if (Objects.nonNull(sessionId)) {
SessionRegistry sessionRegistry = applicationContext.getBean(SessionRegistry.class);
sessionRegistry.removeSessionInformation(sessionId);
}
}
}
/**
* 退出成功處理器
*/
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType(ResponseContent.JSON_UTF8.getValue());
response.setStatus(HttpStatus.OK.value());
response.getWriter().write(objectMapper.writeValueAsString(AuthResponseIO.ok("退出成功!")));
}
}
自定義狀態碼頁面
@Configuration
public class ErrorPageConfig implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
List<ErrorPage> errorPages = new ArrayList<>();
errorPages.add(new ErrorPage(HttpStatus.NOT_FOUND, "/404.html"));
errorPages.add(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500.html"));
registry.addErrorPages(errorPages.toArray(new ErrorPage[0]));
}
}
security bean 配置
將需要注入的bean單獨配置
使用密碼加密參考:鏈接
@Configuration
public class SecurityBean {
/**
* HTTP 防火牆可防止HTTP動詞篡改和跨站點跟蹤
* 參考 <a href="https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)">
*
* @return StrictHttpFirewall
*/
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
//HTTP方法白名單跨站跟蹤(XST)和HTTP動詞篡改
firewall.setAllowedHttpMethods(Arrays.asList(HttpMethod.GET.name(), HttpMethod.POST.name()));
return firewall;
}
/**
* session會話註冊表
*
* @return SessionRegistry
*/
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Autowired
private SecurityProperties securityProperties;
/**
* 處理 rememberMe 自動登錄
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 獲取配置的數據源
DataSource dataSource = applicationContext.getBean(DataSource.class);
jdbcTokenRepository.setDataSource(dataSource);
//設置啓動時創建表
jdbcTokenRepository.setCreateTableOnStartup(securityProperties.isCreateTableOnStartup());
return jdbcTokenRepository;
}
@Autowired
private ApplicationContext applicationContext;
/**
* 密碼策略爲默認,爲了安全希望自定義
* 參考:PasswordEncoderFactories.createDelegatingPasswordEncoder();
* 密碼策略put的內容爲都支持的算法,不安全的算法最好不要支持
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* 重定向策略
*
* @return RedirectStrategy
*/
@Bean
public RedirectStrategy redirectStrategy() {
return new DefaultRedirectStrategy();
}
/**
* Dao認證提供商
*
* @return DaoAuthenticationProvider
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
UserDetailsService userDetailsService = applicationContext.getBean(MyUserDetailsService.class);
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
}
UserDetails
@Getter
@Setter
public class MyUserDetails extends User {
//以後第三方登錄會使用
private LoginType loginType = LoginType.NORMAL;
private String userId;
private String remoteAddress;
public MyUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public MyUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
@Getter
public enum LoginType {
NORMAL, SMS, SOCIAL
}
userDetailsService
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private ApplicationContext applicationContext;
/**
* 需要的數據參考{@link User#withUserDetails}
*
* @param username username
* @return UserDetails
* @throws UsernameNotFoundException e
*/
@Override
public UserDetails loadUserByUsername(String username) {
//SysUser,SysRole,SysPermission爲實體類
//邏輯層SysUserService
SysUserService sysUserService = applicationContext.getBean(SysUserService.class);
//查詢
SysUser sysUser = sysUserService.findByLoginName(username);
if (Objects.nonNull(sysUser)) {
//獲取角色
Set<SysRole> sysRoles = sysUser.getSysRoles();
String[] roleCode = new String[]{};
String[] perCode = new String[]{};
if (Objects.nonNull(sysRoles) && !sysRoles.isEmpty()) {
//角色必須以ROLE_開頭 例如ROLE_ADMIN
roleCode = sysRoles.stream().filter(sysRole -> Objects.nonNull(sysRole.getRoleCode())).map(SysRole::getRoleCode).distinct().toArray(String[]::new);
//具體的權限 看不懂參考jdk8-lambda表達式
List<SysPermission> permissions = sysRoles.stream().
filter(sysRole -> Objects.nonNull(sysRole.getSysPermissions()) && !sysRole.getSysPermissions().isEmpty())
.map(SysRole::getSysPermissions).flatMap(Collection::stream).collect(Collectors.toList());
perCode = permissions.stream().filter(o -> Objects.nonNull(o.getPerCode()))
.map(SysPermission::getPerCode).distinct().toArray(String[]::new);
}
List<GrantedAuthority> roles = AuthorityUtils.createAuthorityList(roleCode);
List<GrantedAuthority> permissions = AuthorityUtils.createAuthorityList(perCode);
roles.addAll(permissions);
MyUserDetails myUserDetails = new MyUserDetails(sysUser.getLoginName(), sysUser.getPassword(),
sysUser.isEnabled(), sysUser.isAccountNonExpired(),
sysUser.isCredentialsNonExpired(), sysUser.isAccountNonLocked(),
roles);
myUserDetails.setUserId(sysUser.getUserId());
return myUserDetails;
} else {
throw new UsernameNotFoundException("未知用戶異常!");
}
}
}
security config 配置
/**
* 瞭解 跨站請求僞造(CSRF)攻擊,動詞篡改和跨站點跟蹤
*/
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({MyUserDetailsService.class, SecurityBean.class, MyAccessDeniedHandler.class, MyAuthenticationSuccessHandler.class,
MyAuthenticationFailureHandler.class, MyLogoutHandler.class,
MyLogoutSuccessHandler.class, MyInvalidSessionStrategy.class,
MySessionInformationExpiredStrategy.class})
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
/**
* 配置忽略路徑
*
* @param web WebSecurity
* @throws Exception e
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(securityProperties.getAnonResourcesUrl());
}
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MyLogoutHandler myLogoutHandler;
@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Autowired
private MyInvalidSessionStrategy myInvalidSessionStrategy;
@Autowired
private MySessionInformationExpiredStrategy mySessionInformationExpiredStrategy;
@Autowired
private SessionRegistry sessionRegistry;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
/**
* 權限認證
*
* @param http http
* @throws Exception e
* @<code> 也可以配置免認證資源:.antMatchers("/resources/**", "/public", "/about").permitAll()。permitAll爲任何用戶都可以請求訪問 </code>
* @<code> 登錄跳轉頁面:loginPage() </code>
* @<code> 登錄成功跳轉:successForwardUrl() </code>
* @<code> 可選項:啓用會話URL重寫(默認false,慎用):enableSessionUrlRewriting()</code>
* @<code> 頁面使用iframe 禁用http.headers().frameOptions().disable()</code>
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/*全部請求需認證*/
http.authorizeRequests().anyRequest().authenticated()
.and()
/*訪問被拒絕處理器*/
.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler)
.and()
/*登錄*/
.formLogin()
.loginPage("/login.html")
.successForwardUrl("index.html")
.loginProcessingUrl(securityProperties.getLoginUrl())
//處理登錄成功
.successHandler(myAuthenticationSuccessHandler)
//處理登錄失敗
.failureHandler(myAuthenticationFailureHandler)
//任何用戶都可以請求訪問
.permitAll()
.and()
/*添加記住我功能(自動登錄)*/
.rememberMe()
.tokenRepository(persistentTokenRepository) // 配置 token 持久化倉庫
.tokenValiditySeconds((int) securityProperties.getSessionTimeout().getSeconds()) // rememberMe 過期時間,單爲秒
.userDetailsService(myUserDetailsService)
.and()
/*配置 session管理器*/
.sessionManagement()
//session過期策略
.invalidSessionStrategy(myInvalidSessionStrategy)
//session過期跳轉url
.invalidSessionUrl(securityProperties.getLoginUrl())
//最大併發登錄數量
.maximumSessions(securityProperties.getMaximumSession())
//最大會話阻止登錄(第二次登錄操作,默認false第二次登錄踢出第一次,true禁止第二次登錄)
.maxSessionsPreventsLogin(false)
//處理併發登錄被踢出
.expiredSessionStrategy(mySessionInformationExpiredStrategy)
.expiredUrl(securityProperties.getLoginUrl())
//配置session註冊中心
.sessionRegistry(sessionRegistry)
.and().and()
/*退出*/
.logout()
//退出時處理器
.addLogoutHandler(myLogoutHandler)
//退出成功處理器
.logoutSuccessHandler(myLogoutSuccessHandler)
.logoutUrl(securityProperties.getLogoutUrl())
.logoutSuccessUrl(securityProperties.getLoginUrl())
//使Http會話無效
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
.and()
.httpBasic();
}
@Autowired
private DaoAuthenticationProvider daoAuthenticationProvider;
/**
* 認證管理生成器
*
* @param auth 認證
* @throws Exception e
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider);
}
}
登錄退出controller不需要配置
註解使用
@EnableGlobalMethodSecurity(prePostEnabled = true) 上面配置有開啓
獲取登錄的信息
public static MyUserDetails getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof MyUserDetails) {
return (MyUserDetails) principal;
}
return null;
}
使用的是SpEL表達式訪問Authentication.getPrincipal()
表達式 | 描述 |
---|---|
|
|
|
返回 |
|
|
|
返回 |
|
允許直接訪問代表當前用戶的主體對象 |
|
允許直接訪問從中獲取的當前 |
|
始終評估爲 |
|
始終評估爲 |
|
|
|
返回 |
|
|
|
返回 |
|
|
|
|
數據角色表role 添加 字段role_code 爲ROLE_ADMIN
包含的權限表permission添加字段per_code 爲:user:create
和用戶配置下關係
注意:在MyUserDetailsService配置中AuthorityUtils.createAuthorityList() 其實按官方是用來配置角色的,我配置爲角色和權限。 這樣使用有一個問題那就是權限修改無法同步到已登錄用戶的session中,解決參考下一篇: security JWT配置。
- hasAuthority:
可以讀取全部的內容,hasAuthority讀取角色需要寫前綴
@RequestMapping("test2")
@PreAuthorize("hasAuthority('user:create')")
public String t2(){
return "菜單權限認證成功";
}
@RequestMapping("test1")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String t(){
return "角色權限認證成功";
}
- hasRole:
讀取角色必須以ROLE_開頭認爲是角色,所以角色讀取去掉前綴。
@RequestMapping("test1")
@PreAuthorize("hasRole('ADMIN')")
public String t(){
return "角色權限認證成功";
}
還可以這樣使用 @PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
- hasPermission:
官方配置需要域對象安全性(ACL)讀取權限,還需依賴
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</dependency>
使用起來比較麻煩採用自定義。
1. 修改MyUserDetailsService
/**
* 需要的數據參考{@link User#withUserDetails}
*
* @param username username
* @return UserDetails
* @throws UsernameNotFoundException e
*/
@Override
public UserDetails loadUserByUsername(String username) {
SysUserService sysUserService = applicationContext.getBean(SysUserService.class);
SysUser sysUser = sysUserService.findByLoginName(username);
if (Objects.nonNull(sysUser)) {
Set<SysRole> sysRoles = sysUser.getSysRoles();
String[] roleCode = new String[]{};
if (Objects.nonNull(sysRoles) && !sysRoles.isEmpty()) {
//數據庫角色碼必須以ROLE_開頭
roleCode = sysRoles.stream().filter(sysRole -> Objects.nonNull(sysRole.getRoleCode())).map(SysRole::getRoleCode).distinct().toArray(String[]::new);
}
MyUserDetails myUserDetails = new MyUserDetails(sysUser.getLoginName(), sysUser.getPassword(),
sysUser.isEnabled(), sysUser.isAccountNonExpired(),
sysUser.isCredentialsNonExpired(), sysUser.isAccountNonLocked(),
AuthorityUtils.createAuthorityList(roleCode));
myUserDetails.setUserId(sysUser.getUserId());
return myUserDetails;
} else {
throw new UsernameNotFoundException("未知用戶異常!");
}
}
}
/**
* hasPermission自定義註解 @PreAuthorize("hasPermission()")的hasPermission認證內容
* fixme:使用注意修改 {@link MyUserDetailsService#loadUserByUsername(String)} 去掉權限碼
*/
@Configuration
public class MyPermissionEvaluator implements PermissionEvaluator {
@Autowired
private ApplicationContext applicationContext;
/**
* 這樣使用的會有一個問題就是每次需要查詢(可以解決權限同步)
*
* @param authentication Authentication
* @param prefix 前綴
* @param suffix 後綴
* @return boolean
*/
@Override
public boolean hasPermission(Authentication authentication, Object prefix, Object suffix) {
SysUserService userService = applicationContext.getBean(SysUserService.class);
boolean flag = false;
//獲取登錄後校驗的用戶數據
Object principal = authentication.getPrincipal();
//判斷是否是MyUserDetails的實例(會有其它類型的登錄例如oauth2)
if (principal instanceof MyUserDetails) {
MyUserDetails userDetails = (MyUserDetails) principal;
//查詢
SysUser user = userService.findByLoginName(userDetails.getUsername());
if (Objects.nonNull(user)) {
Set<SysRole> sysRoles = user.getSysRoles();
if (Objects.nonNull(sysRoles) && !sysRoles.isEmpty()) {
List<String> perCodes = sysRoles.stream().map(SysRole::getSysPermissions)
.filter(Objects::nonNull).flatMap(Collection::stream)
.map(SysPermission::getPerCode).distinct().collect(Collectors.toList());
if (Objects.nonNull(perCodes) && !perCodes.isEmpty()) {
if (Objects.nonNull(prefix) && Objects.nonNull(suffix)) {
String code = prefix.toString() + ":" + suffix.toString();
for (String perCode : perCodes) {
if (Objects.equals(code, perCode)) {
flag = true;
break;
}
}
}
}
}
}
}
return flag;
}
//未使用調用返回false,認證失敗
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
例如:
@RequestMapping("test3")
@PreAuthorize("hasPermission('user','create')")
public String t3(){
return "認證成功";
}
下一篇: security JWT配置(暫更)