【spring系列】spring security開發實踐

​ 之前瞭解瞭如何快速引如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”}

成功和失敗時候有自定義處理器還有自定義跳轉successForwardUrlfailureForwardUrl可根據場景進行選擇

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/mobileget請求到此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")
        ))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章