之前了解了如何快速引如spring security进行项目的权限控制,但是在实际过程中业务要复杂很多。此来了解spring security实战开发实例。
并不是所有的请求都需要权限验证,需要有的请求取消权限验证。
@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"));
}
}
一般前后端分离的时候,后端是没有页面的,如果有各种异常操作,只需要返回状态码即可
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
可根据场景进行选择
默认的表单登录是无法满足我们的需求,比如登录地址修改,登录成功之后返回json。
短信登录
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;
}
}
这个就类似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;
}
}
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);
}
}
短信登录逻辑写好之后,将短信登录的逻辑进行配置。使它生效。
@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);// 这里添加配置
}
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进行验证,防止代码冗余。
单机情况下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共享成功。
登录成功之后需要保存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")
))