spring security前后端分离方案

  • 添加maven依赖
  • spring security返回json配置
  • 自定义密码加密方式
  • 从数据库取用户信息比较用户名密码并填充角色或权限
  • 添加filter实现验证码功能
  • 使用redis存放session信息
  • 开启跨域
  • 测试

1、spring security默认配置下仍然不是前后端分离,当登录成功、失败、访问未登录页面时都会302重定向到另一个页面。显然与我们想要的不同,我们需要返回json
2、spring security如何加入验证码校验
3、配置spring security使用redis存取session信息,符合分布式的规范

添加maven依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

spring security返回json配置

类SecurityConfig,记得要加上@Configuration,当时没加上,调了半天里面的配置都没效果,晕。。。。。。
这里面的http.addFilterBefore(loginAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)添加了验证码功能

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyPasswordEncoder myPasswordEncoder;
    @Autowired
    private UserDetailsService myCustomUserService;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private RedisCache redisCache;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        LoginAuthenticationFilter loginAuthenticationFilter = new LoginAuthenticationFilter(redisCache);
        loginAuthenticationFilter.setAuthenticationManager(authenticationManager());

        http.addFilterBefore(loginAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .authenticationProvider(authenticationProvider())
                .httpBasic()
                //未登录时,进行json格式的提示,很喜欢这种写法,不用单独写一个又一个的类
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType("application/json;charset=utf-8");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    PrintWriter out = response.getWriter();
                    AjaxResult result = new AjaxResult(HttpStatus.FORBIDDEN, "未登录");
                    out.write(objectMapper.writeValueAsString(result));
                    out.flush();
                    out.close();
                })
                .and()
                .authorizeRequests()
                .antMatchers("/favicon.ico", "/captchaImage").permitAll()
                .anyRequest().authenticated() //必须授权才能范围

                .and()
                .exceptionHandling()
                //没有权限,返回json
                .accessDeniedHandler((request, response, ex) -> {
                    response.setContentType("application/json;charset=utf-8");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    PrintWriter out = response.getWriter();
                    AjaxResult result = new AjaxResult(HttpStatus.FORBIDDEN, "权限不足");
                    out.write(objectMapper.writeValueAsString(result));
                    out.flush();
                    out.close();
                })
                .and()
                .logout()
                //退出成功,返回json
                .logoutSuccessHandler((request, response, authentication) -> {
                    AjaxResult result = AjaxResult.success("退出成功");
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.write(objectMapper.writeValueAsString(result));
                    out.flush();
                    out.close();
                })
                .permitAll();
        //激活CorsFilter,自己在CorsFilter中配置允许跨域
        //开启模拟请求,比如API POST测试工具的测试,不开启时,API POST为报403错误
        http.cors().and().csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) {
        //对于在header里面增加token等类似情况,放行所有OPTIONS请求。
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
    }

    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        //对默认的UserDetailsService进行覆盖
        authenticationProvider.setUserDetailsService(myCustomUserService);
        authenticationProvider.setPasswordEncoder(myPasswordEncoder);
        return authenticationProvider;
    }
}

自定义密码加密方式

@Component
public class MyPasswordEncoder implements PasswordEncoder {
    @Value("${password.salt}")
    private String salt;

    @Override
    public String encode(CharSequence charSequence) {
        //加密方法可以根据自己的需要修改
        String s = DigestUtils.md5DigestAsHex((charSequence.toString() + salt).getBytes());
        return s;
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return encode(charSequence).equals(s);
    }
}

继承UserDetails加上自己需要的属性

/**
 * 实现了UserDetails接口,只留必需的属性,也可添加自己需要的属性
 * @author 程就人生
 *
 */
@Component
public class MyUserDetails implements UserDetails {

    /**
     *
     */
    private static final long serialVersionUID = 1L;

    //登录用户名
    private String username;
    //登录密码
    private String password;
    private Integer userId;

    private Collection<? extends GrantedAuthority> authorities;

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

从数据库取用户信息比较用户名密码并填充角色或权限

@Component
public class MyCustomUserService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleMapper roleMapper;

    /**
     * 登陆验证时,通过username获取用户的所有权限信息
     * 并返回UserDetails放到spring的全局缓存SecurityContextHolder中,以供授权器使用
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //在这里可以自己调用数据库,对username进行查询,看看在数据库中是否存在
        MyUserDetails myUserDetail = new MyUserDetails();
        LambdaQueryWrapper<User> userWrapper = Wrappers.<User>lambdaQuery();
        userWrapper.eq(User::getUsername, username).eq(User::getDisabled, false);
        User user = userMapper.selectOne(userWrapper);
        if (user == null) {
            throw new UsernameNotFoundException("找不到用户信息");
        }
        myUserDetail.setUsername(user.getUsername());
        myUserDetail.setPassword(user.getPassword());
        myUserDetail.setUserId(user.getUserId());

        List<String> roleKeys = roleMapper.selectRoleKeyByUserId(user.getUserId());
        List<GrantedAuthority> list = new ArrayList<>();
        roleKeys.forEach(o -> list.add(new SimpleGrantedAuthority("ROLE_" + o)));
        myUserDetail.setAuthorities(list);
        return myUserDetail;
    }
}

添加filter实现验证码功能

写一个filter继承UsernamePasswordAuthenticationFilter,验证码校验

public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private RedisCache redisCache;

    public LoginAuthenticationFilter(RedisCache redisCache) {
        ObjectMapper objectMapper = new ObjectMapper();
        this.redisCache = redisCache;

        AntPathRequestMatcher requestMatcher = new AntPathRequestMatcher("/login", "POST");
        this.setRequiresAuthenticationRequestMatcher(requestMatcher);
        this.setAuthenticationManager(getAuthenticationManager());
        this.setAuthenticationSuccessHandler((request, response, authentication) -> {
            User user = new User();
            user.setUserId(((MyUserDetails)authentication.getPrincipal()).getUserId());
            user.setLogin_date(new Date());
            user.setLogin_ip(request.getRemoteHost());
            user.updateById();

            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write(objectMapper.writeValueAsString(AjaxResult.success("登录成功", authentication.getAuthorities())));
            out.flush();
            out.close();

        });
        this.setAuthenticationFailureHandler((request, response, ex) -> {
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            PrintWriter out = response.getWriter();
            AjaxResult result = null;
            if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) {
                result = new AjaxResult(401, "用户名或密码错误");
            } else if (ex instanceof DisabledException) {
                result = new AjaxResult(401, "账户被禁用");
            } else if (ex instanceof NotMatchCaptchaException) {
                result = new AjaxResult(401, "验证码不正确");
            } else if (ex instanceof InvalidCaptchaException) {
                result = new AjaxResult(401, "验证码失效,请刷新");
            } else {
                result = new AjaxResult(401, "登录失败!");
            }
            out.write(objectMapper.writeValueAsString(result));
            out.flush();
            out.close();
        });
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String verification = request.getParameter("code");
        String uuid = request.getParameter("uuid");
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
        String captcha = redisCache.getCacheObject(verifyKey);
        redisCache.deleteObject(verifyKey);
        if (captcha == null) {
            throw new InvalidCaptchaException();
        }
        if (!captcha.equalsIgnoreCase(verification)) {
            throw new NotMatchCaptchaException();
        }
        return super.attemptAuthentication(request, response);
    }

}

使用redis存放session信息

/**
 * redis实现session共享
 */
@Configuration
@EnableRedisHttpSession
public class SessionConfig {

}

使用redis不要忘记序列化配置

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        template.setValueSerializer(serializer);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    @SuppressWarnings("unused")
    private ObjectMapper objectMapper = new ObjectMapper();

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJson2JsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }

    public void setObjectMapper(ObjectMapper objectMapper)
    {
        Assert.notNull(objectMapper, "'objectMapper' must not be null");
        this.objectMapper = objectMapper;
    }

    protected JavaType getJavaType(Class<?> clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

开启跨域

便于前端调度开启跨域

@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }
}

测试

@PreAuthorize("hasAnyRole('ROLE_crop')")
    @RequestMapping("/test1")
    public AjaxResult test1() {
        return AjaxResult.success("test1");
    }

    @PreAuthorize("hasAnyRole('ROLE_test')")
    @RequestMapping("/test2")
    public AjaxResult test2() {
        return AjaxResult.success("test2");
    }