【spring系列】spring security(2)开发实践

​ 之前了解了如何快速引如spring security进行项目的权限控制,但是在实际过程中业务要复杂很多。此来了解spring security实战开发实例。

文章目录

  • 1.自定义过滤
  • 2.成功与异常之后自定义处理
  • 3.自定义登录-短信登录
    • 3.1创建短信登录token
    • 3.2 创建短信登录Filter
    • 3.3创建短信Provider
    • 3.4配置短信Filter
  • 4.登录前验证
  • 5.session共享
  • 5.注销登录

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.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.sessiongroupId>
            <artifactId>spring-session-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
        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共享成功。
【spring系列】spring security(2)开发实践_第1张图片

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")
        ))

你可能感兴趣的:(spring系列,spring,security,权限)