之前瞭解瞭如何快速引如spring security進行項目的權限控制,但是在實際過程中業務要複雜很多。此來了解spring security實戰開發實例。
文章目錄
1.自定義過濾
並不是所有的請求都需要權限驗證,需要有的請求取消權限驗證。
@Configuration
public class ExampleSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表單登錄
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll() // 此請求不需要進行驗證
.antMatchers("/userinfo/update").hasRole("admin") // 有admin權限纔可以
//.antMatchers("").permitAll() 這裏可以寫多個,也可以進行正則表達式
//.antMatchers("").permitAll()
//.antMatchers("").permitAll()
.anyRequest()
.authenticated();
}
}
如此可以完成某些請求不需要驗證。
注:hasRole這個是在UserDetailsService
的實現中進行授權的。但是權限信息默認會有ROLE_
作爲開頭。
@Component
public class UserAuthService implements UserDetailsService {
@Autowired
public PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
System.out.println("登錄用戶信息:"+userName);
// todo 此處根據用戶信息查詢賬號密碼,這裏返回 111111
// 用戶類型爲admin
// 這裏直接進行加密操作,實際上是從數據庫查詢出來的加密字符串
return new User(userName,passwordEncoder.encode("111111"),
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin"));
}
}
2.成功與異常之後自定義處理
一般前後端分離的時候,後端是沒有頁面的,如果有各種異常操作,只需要返回狀態碼即可
SimpleUrlAuthenticationFailureHandler請求失敗處理類,在spring security
認證失敗後會進入此類,在此類中可以自定義認證失敗邏輯。前後端分離時可進行自定義異常返回。
@Component
public class AuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
// 這裏可以寫更復雜的認證錯誤的邏輯
response.getWriter().write("登錄失敗");
}
}
SavedRequestAwareAuthenticationSuccessHandler請求成功處理類。spring security
在請求成功時,會進入這裏,可以自定義返回用戶信息。
@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登錄成功");
// 這裏把認證之後的信息進行返回
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
寫好成功和失敗的邏輯之後進行配置。
@Configuration
public class ExampleSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private SmsCodeAuthenticationSecurityConfig codeAuthenticationSecurityConfig;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
//設置成功和失敗的處理類
http.formLogin().successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers("/login/**").permitAll()
.antMatchers("/error/**").permitAll()
.antMatchers("/userinfo/update").hasRole("aaa")
.anyRequest()
.authenticated()
.and()
.apply(codeAuthenticationSecurityConfig);
}
}
成功登陸之後返回:
{“authorities”:[{“authority”:“ROLE_admin”}],“details”:{“remoteAddress”:“0:0:0:0:0:0:0:1”,“sessionId”:“9D02A1032740267FC1392647ADF31166”},“authenticated”:true,“principal”:{“password”:null,“username”:“admin”,“authorities”:[{“authority”:“ROLE_admin”}],“accountNonExpired”:true,“accountNonLocked”:true,“credentialsNonExpired”:true,“enabled”:true},“credentials”:null,“name”:“admin”}
成功和失敗時候有自定義處理器還有自定義跳轉successForwardUrl
和failureForwardUrl
可根據場景進行選擇
3.自定義登錄-短信登錄
默認的表單登錄是無法滿足我們的需求,比如登錄地址修改,登錄成功之後返回json。
短信登錄
3.1創建短信登錄token
token是用來存儲用戶信息的。
public class MsgToken extends AbstractAuthenticationToken {
private final String mobile;
public MsgToken(String mobile) {
// 未認證時
super(null);
this.mobile = mobile;
//未授權
super.setAuthenticated(false);
}
public MsgToken(String mobile,Collection<? extends GrantedAuthority> authorities ) {
// 認證成功傳如權限信息
super(authorities);
this.mobile = mobile;
// 成功授權
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
public String getPrincipal() {
return mobile;
}
}
3.2 創建短信登錄Filter
這個就類似UsernamePasswordAuthenticationFilter
主要是攔截請求到此。
設置路徑爲/auth/mobile
的get
請求到此filter。
public class MsgCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//請求參數
private String usernameParameter = "mobile";
private String code="code";
// 請求路徑
public MsgCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/auth/mobile", "GET"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String mobile = request.getParameter(this.usernameParameter);
String code = request.getParameter(this.code);
if(mobile == null) {
mobile = "";
}
mobile = mobile.trim();
//todo 驗證短信是否有效,驗證成功則繼續執行,失敗拋異常
if(!"123456".equals(code)){
throw new UsernameNotFoundException("1231111");
}
MsgToken authRequest = new MsgToken(mobile);
//將請求的信息設置到token中
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected void setDetails(HttpServletRequest request, MsgToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
}
3.3創建短信Provider
public class MsgAuthProvider implements AuthenticationProvider {
public MsgAuthProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
// 此類信息在上文中有
private UserDetailsService userDetailsService;
//如果token類型爲MsgToken,則進入此provider。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MsgToken msgToken = (MsgToken) authentication;
String mobile = msgToken.getPrincipal();
// 獲取用戶信息
UserDetails userDetails = this.userDetailsService.loadUserByUsername(mobile);
//創建token,此token是認證成功的token。
MsgToken msgAuth = new MsgToken(mobile,userDetails.getAuthorities());
// 將用戶信息設置到token中
msgAuth.setDetails(userDetails);
return msgAuth;
}
@Override
public boolean supports(Class<?> aClass) {
//通過token的類判斷是否進入此provider
return MsgToken.class.isAssignableFrom(aClass);
}
}
3.4配置短信Filter
短信登錄邏輯寫好之後,將短信登錄的邏輯進行配置。使它生效。
@Component
public class MsgCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private UserAuthService userDetailsService;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
MsgCodeAuthenticationFilter smsCodeAuthenticationFilter = new MsgCodeAuthenticationFilter();
//設置要被管理的manager
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//設置成功和失敗邏輯
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
MsgAuthProvider smsCodeAuthenticationProvider = new MsgAuthProvider(userDetailsService);
//加入到UsernamePasswordAuthenticationFilter後面
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
修改ExampleSecurityConfig
@Autowired
private MsgCodeAuthenticationSecurityConfig msgCodeAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers("/login/**").permitAll()
.antMatchers("/error/**").permitAll()
.antMatchers("/userinfo/update").hasRole("aaa")
.anyRequest()
.authenticated()
.and()
.apply(msgCodeAuthenticationSecurityConfig);// 這裏添加配置
}
4.登錄前驗證
spring security
是很多的Filter組成的,我們可以在Filter鏈上進行添加自己的Filter。比如在輸入賬號密碼前輸入手機驗證碼或者圖片驗證碼。
創建驗證Filter
@Component
public class ValidateCodeMsgFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//todo 驗證碼驗證
// 這裏簡易版 驗證碼寫死爲123456
if("/auth/mobile".equals(request.getRequestURI())){
String code = request.getParameter("code");
if(!"123456".equals(code)){
throw new UsernameNotFoundException("1231111");
}
}
filterChain.doFilter(request,httpServletResponse);
}
}
配置此Filter在短信驗證Filter之前。
@Autowired
private ValidateCodeMsgFilter validateCodeMsgFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
MsgCodeAuthenticationFilter smsCodeAuthenticationFilter = new MsgCodeAuthenticationFilter();
//設置要被管理的manager
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//設置成功和失敗邏輯
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
MsgAuthProvider smsCodeAuthenticationProvider = new MsgAuthProvider(userDetailsService);
//加入到UsernamePasswordAuthenticationFilter後面
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(validateCodeMsgFilter,MsgCodeAuthenticationFilter.class);// 這裏是進行添加驗證碼驗證
}
其實短信驗證碼驗證邏輯寫在短信登錄Filter中也可以。但是隨着業務邏輯的逐漸複雜,如果普通賬號密碼登錄也需要驗證碼時,就需要單獨提取出來一個Filter進行驗證,防止代碼冗餘。
5.session共享
單機情況下session幾乎不需要處理,若多個服務的時候,就要實現session共享。
redis實現session共享
添加redis依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
添加redis配置信息
#通過redis進行保存session
spring.session.store-type = redis
#Redis
spring.redis.host=127.0.0.1
## Redis服務器連接端口
spring.redis.port=6379
## 連接超時時間(毫秒)
spring.redis.timeout=300ms
## 連接池中的最大連接數
spring.redis.jedis.pool.max-idle=10
## 連接池中的最大空閒連接
spring.redis.lettuce.pool.max-idle=8
## 連接池中的最大阻塞等待時間
spring.redis.jedis.pool.max-wait=-1ms
## 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.lettuce.shutdown-timeout=100ms
server.servlet.session.timeout=2000s
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session
直接複製走即可
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
/**
* 配置連接工廠
* @return
*/
@Bean(name = "factory")
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHost, redisPort);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
/**
* 配置緩存管理器
* @param factory 連接工廠
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
return new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(factory),
RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)).disableCachingNullValues());
}
/**
* Redis操作模板
* @param factory 連接工廠
* @return
*/
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
配置好之後,啓動redis,通過登錄,登錄完成之後redis如果有spring:session
開頭的key,證明session共享成功。
5.註銷登錄
登錄成功之後需要保存session,註銷登錄需要把session進行清理
註銷地址修改
默認註銷登錄的地址是/logout
,修改註銷地址。
http.formLogin().and().logout().logoutUrl("/mylogout").logoutSuccessUrl("/logoutsuccess")
註銷成功之後可以自定義處理logoutSuccessUrl
或者logoutSuccessHandler
,從名字上可以看出來,一個是跳轉地址,一個是實現處理器。
註銷成功處理器。
@Component
public class AuthLogoutSuccessHandler implements LogoutSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
logger.info("註銷登錄");
System.out.println("註銷登錄");
PrintWriter writer = httpServletResponse.getWriter();
writer.print(1111);
writer.flush();
writer.close();
}
}
/mylogout
地址默認的可能是POST請求,如果瀏覽器直接訪問可能訪問不成功。如此配置即可
http.formLogin().and().logout().logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/mylogout", "GET")
))