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