描述:
網上代碼大部分都使用實體類實現 UserDetails,同時實現 UserDetailsService,對於不熟悉 security 的開發人員在開發登錄接口時不清楚需要實現這兩個接口,對於在配置權限時,需要重構登錄接口。
如果不想實現 UserDetailsService 需要自己實現 AuthenticationProvider 接口
AuthenticationProvider 接口
- authenticate 方法來驗證,就是驗證用戶身份
- supports 用來判斷當前 AuthenicationProvider 是否對應支持 Authentication
代碼部分如下
由於在 springboot2.7 版本及之後版本使用的 security5.7.1,該版本中之前需要繼承的 WebSecurityConfigurerAdapter 類已經過時,所以使用最新配置方式如下
securityConfig
package smart.property.admin.controller.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import smart.property.admin.controller.filter.*;
import java.util.ArrayList;
import java.util.List;
/**
* BUG不找我
*
* @author huatao
* @date 2023/4/1 15:40
*/
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Autowired
private MyAuthenticationProvider myAuthenticationProvider;
@Autowired
private SelfAccessDecisionManager selfAccessDecisionManager;
@Autowired
private SelfFilterInvocationSecurityMetadataSource selfFilterInvocationSecurityMetadataSource;
/**
* 獲取AuthenticationManager(認證管理器),登錄時認證使用
*
* @param authenticationConfiguration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
final List<GlobalAuthenticationConfigurerAdapter> configurers = new ArrayList<>();
//自定義驗證賬戶密碼
configurers.add(new GlobalAuthenticationConfigurerAdapter() {
@Override
public void configure(AuthenticationManagerBuilder auth){
// auth.doSomething()
auth.authenticationProvider(myAuthenticationProvider);
}
}
);
authenticationConfiguration.setGlobalAuthenticationConfigurers(configurers);
return authenticationConfiguration.getAuthenticationManager();
}
/**
* security 配置
*
* @param httpSecurity
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setSecurityMetadataSource(selfFilterInvocationSecurityMetadataSource); //動態獲取url權限配置
o.setAccessDecisionManager(selfAccessDecisionManager); //權限判斷
return o;
}
})
.and()
.formLogin() //關閉form提交方式
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)//禁用session
.and()
.httpBasic()
.authenticationEntryPoint(new UnLoginUserEntryPoint())
// .userDetailsService(userDetailsService)
// .cors().disable()
// .httpBasic()
.and()
.csrf().disable()
// .cors().disable()
;
return httpSecurity.build();
}
/**
* 釋放路徑
*
* @return
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return new WebSecurityCustomizer() {
@Override
public void customize(WebSecurity web) {
web.ignoring().antMatchers("/login/**");// 放行login開頭路徑
}
};
}
/**
* 加密方式注入容器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定義 AuthenticationProvider
package smart.property.admin.controller.filter;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
/**
* BUG不找我
* 重定義賬戶密碼驗證過濾器
*
* @author huatao
* @date 2023/4/6 14:43
*/
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//由於我們在service已經驗證過賬戶密碼等操作,這裏直接組裝返回數據即可
return new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), null, authentication.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}
FilterInvocationSecurityMetadataSource 實現類
package smart.property.admin.controller.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import smart.property.admin.bean.menu.MenuInfoPO;
import smart.property.admin.bean.role.RoleInfoPO;
import smart.property.admin.service.menu.MenuInfoService;
import java.util.Collection;
import java.util.List;
/**
* BUG不找我
* 獲取當前路徑所需要的權限
* @author huatao
* @date 2022/7/18 12:00
*/
@Slf4j
@Component
public class SelfFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private MenuInfoService menuInfoService;
@Autowired
public SelfFilterInvocationSecurityMetadataSource(MenuInfoService menuInfoService) {
this.menuInfoService = menuInfoService;
}
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//獲取請求路徑
String requestUrl = ((FilterInvocation) o).getRequestUrl();
log.info("當前請求路徑:{}",requestUrl);
//獲取所有路徑以及對應角色
List<MenuInfoPO> urlList = menuInfoService.getUrlList();
//聲明保存可訪問角色集合
String[] str = null;
//獲取每個路徑所需要的角色
for(MenuInfoPO menuUrlVO:urlList){
//判斷請求路徑和配置路徑是否一致,一致則獲取該路徑所需要的角色
if(antPathMatcher.match(menuUrlVO.getMenuUri(),requestUrl)){
List<RoleInfoPO> roleInfoPOList = menuUrlVO.getRoleInfoPOList();
//判斷路徑可以訪問的角色是否爲null
str=new String[roleInfoPOList.size()];
for (int i=0; i<roleInfoPOList.size(); i++){
str[i] = roleInfoPOList.get(i).getRoleName();
}
}
};
//判斷是否爲空,如果爲空直接拋出無權限異常,否則會進入接口
if(null==str||str.length<1){
throw new AccessDeniedException("權限不足,請聯繫管理員!");
}
return SecurityConfig.createList(str);
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return FilterInvocation.class.isAssignableFrom(aClass);
}
}
AccessDecisionManager
package smart.property.admin.controller.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;
import smart.property.admin.bean.login.LoginUserVO;
import smart.property.admin.bean.role.RoleNameVO;
import smart.property.admin.bean.sys.RedisPrefix;
import smart.property.admin.service.role.RoleInfoService;
import smart.property.utils.token.TokenManager;
import smart.property.utils.json.JacksonUtil;
import java.util.Collection;
import java.util.List;
/**
* BUG不找我
* 判斷當前用戶是否具有權限
* @author huatao
* @date 2022/7/18 12:03
*/
@Slf4j
@Component
public class SelfAccessDecisionManager implements AccessDecisionManager {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RoleInfoService roleInfoService;
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//從請求頭獲取token
String token = ((FilterInvocation) o).getHttpRequest().getHeader("token");
if(HttpMethod.OPTIONS.toString().equals(((FilterInvocation) o).getHttpRequest().getMethod())){
return;
}
// 通過token獲取加密信息
String subjectByToken = TokenManager.getSubjectByToken(token);
//將json轉爲登錄用戶
LoginUserVO loginUserVO = JacksonUtil.toBean(subjectByToken, LoginUserVO.class);
//通過用戶名獲取用戶信息
String roleInfo = stringRedisTemplate.opsForValue().get(RedisPrefix.USER_ROLE + loginUserVO.getUsername());
List<RoleNameVO> roleNameVOList =null;
//判斷如果redis不存在用戶角色則從數據庫查詢,如果存在則直接轉爲角色集合
if(StringUtils.isBlank(roleInfo)){
roleNameVOList = roleInfoService.getRoleByUserId(loginUserVO.getId());
stringRedisTemplate.opsForValue().set(RedisPrefix.USER_ROLE + loginUserVO.getUsername(),JacksonUtil.toString(roleNameVOList));
}else{
roleNameVOList = JacksonUtil.jsonToBeanList(roleInfo, RoleNameVO.class);
}
for (ConfigAttribute configAttribute : collection) {
String needRole = configAttribute.getAttribute();
for (int i=0;i<roleNameVOList.size();i++ ) {
if (roleNameVOList.get(i).getRoleName().equals(needRole)){
return;
}
}
}
//無訪問權限
throw new AccessDeniedException("無訪問權限");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
AuthenticationEntryPoint
package smart.property.admin.controller.filter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import smart.property.admin.bean.sys.R;
import smart.property.admin.bean.sys.RespEnum;
import smart.property.utils.json.JacksonUtil;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* BUG不找我
*
* @author huatao
* @date 2023/4/6 14:27
*/
public class UnLoginUserEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
R error = R.error(RespEnum.ERROR_NO_ROLE.getCode(), RespEnum.ERROR_NO_ROLE.getMessage());
PrintWriter writer = response.getWriter();
writer.print(JacksonUtil.toString(error));
writer.flush();
writer.close();
}
}
登錄處理接口
/**
* BUG不找我
* 登錄業務
* @author huatao
* @date 2023/4/1 9:30
*/
@RestController
@RequestMapping("login")
public class LoginController {
private final StringRedisTemplate stringRedisTemplate;
private final LoginService loginService;
@Autowired
public LoginController(StringRedisTemplate stringRedisTemplate,LoginService loginService) {
this.stringRedisTemplate = stringRedisTemplate;
this.loginService = loginService;
}
/**
* 獲取驗證碼
* @return
*/
@GetMapping("getCode")
public R<CodeVO> getCode(){
return R.success(loginService.getCode());
}
/**
* 登錄
* @param loginDTO
* @return
*/
@PostMapping("doLogin")
public R<LoginUserVO> doLogin(@Valid @RequestBody LoginDTO loginDTO, HttpServletResponse response){
LoginUserVO loginUserVO = loginService.doLogin(loginDTO);
//創建token
response.setHeader("token", TokenManager.createToken(JacksonUtil.toString(loginUserVO)));
//創建刷新token
response.setHeader("refreshToken",TokenManager.createRefreshToken(JacksonUtil.toString(loginUserVO)));
return R.success(loginUserVO);
}
}
package smart.property.admin.service.login;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.ShearCaptcha;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import smart.property.admin.bean.login.CodeVO;
import smart.property.admin.bean.login.LoginDTO;
import smart.property.admin.bean.login.LoginUserVO;
import smart.property.admin.bean.role.RoleInfoPO;
import smart.property.admin.bean.role.RoleNameVO;
import smart.property.admin.bean.sys.RedisPrefix;
import smart.property.admin.bean.sys.SysException;
import smart.property.admin.bean.user.UserInfoPO;
import smart.property.admin.mapper.user.UserMapper;
import smart.property.admin.service.role.RoleInfoService;
import smart.property.utils.convert.ConvertUtils;
import smart.property.utils.json.JacksonUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* BUG不找我
* 自定義登錄
*
* @author huatao
* @date 2023/4/1 16:05
*/
@Service
public class LoginServiceImpl implements LoginService{
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final StringRedisTemplate stringRedisTemplate;
private final AuthenticationManager authenticationManager;
private final RoleInfoService roleInfoService;
@Autowired
public LoginServiceImpl(UserMapper userMapper, PasswordEncoder passwordEncoder, StringRedisTemplate stringRedisTemplate, AuthenticationManager authenticationManager,RoleInfoService roleInfoService) {
this.userMapper = userMapper;
this.passwordEncoder = passwordEncoder;
this.stringRedisTemplate = stringRedisTemplate;
this.authenticationManager = authenticationManager;
this.roleInfoService = roleInfoService;
}
@Override
public LoginUserVO doLogin(LoginDTO loginDTO) {
UserInfoPO userByUsername = userMapper.getUserByUsername(loginDTO.getUsername());
//獲取用戶擁有的角色
List<RoleNameVO> roleByUserId = roleInfoService.getRoleByUserId(userByUsername.getId());
stringRedisTemplate.opsForValue().set(RedisPrefix.USER_ROLE+userByUsername.getUsername(), JacksonUtil.toString(roleByUserId),2,TimeUnit.HOURS);
//3使用ProviderManager auth方法進行驗證
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userByUsername.getUsername(), userByUsername.getPassword());
authenticationManager.authenticate(usernamePasswordAuthenticationToken);
return ConvertUtils.convert(userByUsername, LoginUserVO.class);
}
}
判斷是否有token
package smart.property.admin.controller.filter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import smart.property.admin.bean.sys.R;
import smart.property.admin.bean.sys.RespEnum;
import smart.property.admin.bean.sys.SysException;
import smart.property.utils.json.JacksonUtil;
import smart.property.utils.token.TokenManager;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* BUG不找我
* 判斷請求是否包含請求頭
*
* @author huatao
* @date 2023/4/13 16:22
*/
@Component
@Order(-100000)
public class TokenFilter implements Filter {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
filterChain.doFilter(servletRequest, servletResponse);
} else if (antPathMatcher.match("/admin/login/**", request.getRequestURI())) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
servletResponse.setCharacterEncoding("UTF-8");
PrintWriter writer = servletResponse.getWriter();
writer.print(JacksonUtil.toString(R.error(RespEnum.ERROR_NO_LOGIN.getCode(), RespEnum.ERROR_NO_LOGIN.getMessage())));
writer.flush();
writer.close();
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
@Override
public void destroy() {
}
}
跨域處理
package smart.property.admin.controller.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* BUG不找我
* 處理跨域
* @author huatao
* @date 2023/4/1 19:49
*/
@Configuration
public class CorsConfig {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("http://127.0.0.1:8080"); // 1允許任何域名使用
corsConfiguration.addAllowedOrigin("http://localhost:8080"); // 1允許任何域名使用
corsConfiguration.addAllowedHeader("*"); // 2允許任何頭
corsConfiguration.addAllowedMethod("*"); // 3允許任何方法(post、get等)
source.registerCorsConfiguration("/**", corsConfiguration);
//如果沒有下面兩行則會出現無權限訪問時會出現跨域錯誤
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new CorsFilter(source));
// 代表這個過濾器在衆多過濾器中級別最高,也就是過濾的時候最先執行
filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filterRegistrationBean;
}
}
不對的地方歡迎指出!~
轉載請標明地址謝謝!!!