一,导入依赖
<dependency>
<groupId>org.springframework.boot
</groupId>
<artifactId>spring-boot-starter-security
</artifactId>
</dependency>
二,自定义userDetail 和userDetailService
userDetail 是security 中对用户信息的封装,security默认使用DaoAuthenticationProvider 去调用userDetailService 获取数据库中的用户信息,然后拿着这个信息去和前端传来的用户信息进行比对,完成认证。
userDetail
public class SelfUserDetails implements UserDetails, Serializable
{
private String username
;
private String password
;
private Set
<? extends GrantedAuthority> authorities
;
@Override
public Collection
<? extends GrantedAuthority> getAuthorities() {
return this.authorities
;
}
public void setAuthorities(Set
<? extends GrantedAuthority> authorities
) {
this.authorities
= authorities
;
}
@Override
public String
getPassword() {
return this.password
;
}
@Override
public String
getUsername() {
return this.username
;
}
public void setUsername(String username
) {
this.username
= username
;
}
public void setPassword(String password
) {
this.password
= password
;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
userDetailService
@Component
@Slf4j(topic
= "security登陆认证")
public class SelfUserDetailsService implements UserDetailsService {
@Autowired
private TUsersDao userDao
;
@Autowired
private AgentInfoDao agentInfoDao
;
@Override
public UserDetails
loadUserByUsername(String username
) throws UsernameNotFoundException
{
SelfUserDetails userInfo
= new SelfUserDetails();
TUsers users
= userDao
.findByUsername(username
);
if (users
== null
)
users
= userDao
.findByMobile(username
);
if (users
== null
)
throw new UsernameNotFoundException("用户不存在");
userInfo
.setUsername(users
.getUsername());
userInfo
.setPassword(users
.getPassword());
Set
<GrantedAuthority> authoritiesSet
= new HashSet<>();
GrantedAuthority authority
= null
;
TAgentInfo agentInfo
= agentInfoDao
.findByUserId(users
.getId());
if ( agentInfo
== null
){
authority
= new SimpleGrantedAuthority(Constant
.CmsRole
.USER
);
}else {
if (agentInfo
.getCheckStatus() != 1){
authority
= new SimpleGrantedAuthority(Constant
.CmsRole
.USER
);
}else {
authority
= new SimpleGrantedAuthority(Constant
.CmsRole
.AGENT
);
}
}
authoritiesSet
.add(authority
);
userInfo
.setAuthorities(authoritiesSet
);
log
.info("数据库用户名:'{}',用户角色:'{}'",users
.getUsername(),authority
.getAuthority());
return userInfo
;
}
}
三, 自定义handler
security 对于登陆失败和登陆成功,权限不足都有各自的jandler,前后端分离模式下,需要重新定义。
AccessDeniedHandler (登陆之后发生的权限不足)
@Component
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse
, AccessDeniedException e
) throws IOException
, ServletException
{
ResponseBean responseBody
= new ResponseBean();
responseBody
.setCode(300);
responseBody
.setMsg("权限不足,联系管理员!");
httpServletResponse
.setCharacterEncoding("utf-8");
httpServletResponse
.setContentType("application/json;charset=utf-8");
httpServletResponse
.getWriter().write(JSON
.toJSONString(responseBody
));
}
}
AuthenticationEntryPoint(未登录权限不足)
@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse
, AuthenticationException e
) throws IOException
, ServletException
{
ResponseBean responseBody
= new ResponseBean();
responseBody
.setCode(400);
responseBody
.setMsg("没有权限,请先登陆");
httpServletResponse
.setCharacterEncoding("utf-8");
httpServletResponse
.setContentType("application/json;charset=utf-8");
httpServletResponse
.getWriter().write(JSON
.toJSONString(responseBody
));
}
}
AuthenticationFailureHandler(登陆失败处理器)
@Component
public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse
, AuthenticationException e
) throws IOException
, ServletException
{
ResponseBean responseBody
= new ResponseBean();
responseBody
.setCode(400);
responseBody
.setMsg("登陆失败!");
httpServletResponse
.setCharacterEncoding("utf-8");
httpServletResponse
.setContentType("application/json;charset=utf-8");
httpServletResponse
.getWriter().write(JSON
.toJSONString(responseBody
));
}
}
AuthenticationSuccessHandler(登陆成功处理器)
@Component
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse
, Authentication authentication
) throws IOException
, ServletException
{
ResponseBean responseBody
= new ResponseBean();
responseBody
.setCode(200);
responseBody
.setMsg("登陆成功!");
httpServletResponse
.setCharacterEncoding("utf-8");
httpServletResponse
.setContentType("application/json;charset=utf-8");
httpServletResponse
.getWriter().write(JSON
.toJSONString(responseBody
));
}
}
LogoutSuccessHandler(退出成功处理器)
@Component
public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse
, Authentication authentication
) throws IOException
, ServletException
{
ResponseBean responseBody
= new ResponseBean();
responseBody
.setCode(200);
responseBody
.setMsg("退出成功!");
httpServletResponse
.setCharacterEncoding("utf-8");
httpServletResponse
.setContentType("application/json;charset=utf-8");
httpServletResponse
.getWriter().write(JSON
.toJSONString(responseBody
));
}
}
## 四,配置类
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled
=true)
public class SpringSecurityConf extends WebSecurityConfigurerAdapter {
@Autowired
AjaxAuthenticationEntryPoint authenticationEntryPoint
;
@Autowired
AjaxAuthenticationSuccessHandler authenticationSuccessHandler
;
@Autowired
AjaxAuthenticationFailureHandler authenticationFailureHandler
;
@Autowired
AjaxLogoutSuccessHandler logoutSuccessHandler
;
@Autowired
AjaxAccessDeniedHandler accessDeniedHandler
;
@Autowired
private SelfUserDetailsService userDetailsService
;
@Autowired
private SmsProvider smsProvider
;
@Override
protected void configure(AuthenticationManagerBuilder auth
) throws Exception
{
auth
.authenticationProvider(smsProvider
);
auth
.userDetailsService(userDetailsService
).passwordEncoder(new BCryptPasswordEncoder());
auth
.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("xch")
.password(new BCryptPasswordEncoder().encode("123")).roles(Constant
.CmsRole
.ADMIN
);
}
@Bean
GrantedAuthorityDefaults
grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("");
}
@Override
protected void configure(HttpSecurity http
) throws Exception
{
http
.csrf().disable()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler
)
.failureHandler(authenticationFailureHandler
)
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**","/error","/doc.html").permitAll()
.antMatchers(
Constant
.RedisKey
.PC_REG_SMS
,
Constant
.RedisKey
.PC_LOG_SMS
,"/sms/**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler
)
.permitAll();
http
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint
)
.accessDeniedHandler(accessDeniedHandler
); // 无权访问 JSON 格式的数据
SmsAuthenticationFilter smsAuthenticationFilter
= new SmsAuthenticationFilter(Constant
.RedisKey
.PC_LOG_SMS
);
smsAuthenticationFilter
.setAuthenticationSuccessHandler(authenticationSuccessHandler
);
smsAuthenticationFilter
.setAuthenticationFailureHandler(authenticationFailureHandler
);
smsAuthenticationFilter
.setAuthenticationManager(authenticationManagerBean());
http
.addFilterBefore(smsAuthenticationFilter
,UsernamePasswordAuthenticationFilter
.class);
}
@Bean
@Override
public AuthenticationManager
authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
}
五, 新增短信登陆入口
security 认证思路:
manager 管理多个provider,一个provider通过surpports可以验证多个或一个token,一般来说,一个应用只有一个manager,authenticationFilter 构建未验证的token,交由manager,manager去寻找特定的provider去验证token。验证成功后的token将放入SecurityContextHolder(Threadlocal的封装)中,然后请求结束时,security有个过滤器,会将验证之后的token放入session中,下次请求便不用重新登陆。
AuthenticationFilter
AuthenticationManager
AuthenticationProvider
返回认证之后的AuthenticationToken
自定义一套短信登陆的filter,provider
之前security配置开启表单登陆,默认使用security自带的DaoAuthenticationProvider ,UsernamePasswordAuthenticationFilter。新增短信登陆需要自己实现一套这样的机制。
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public SmsAuthenticationFilter(String defaultFilterProcessesUrl
) {
super(defaultFilterProcessesUrl
);
}
@Override
public Authentication
attemptAuthentication(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse
) throws AuthenticationException
, IOException
, ServletException
{
String uri
= httpServletRequest
.getRequestURI();
String mobile
= null
;
String code
= null
;
if (uri
.equals(Constant
.RedisKey
.PC_LOG_SMS
) && httpServletRequest
.getMethod().equals("POST")) {
mobile
= httpServletRequest
.getParameter("mobile");
code
= httpServletRequest
.getParameter("code");
if (mobile
== null
) {
mobile
= "";
}
mobile
= mobile
.trim();
SmsToken smsToken
= new SmsToken(mobile
, code
);
smsToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest
));
return this.getAuthenticationManager().authenticate(smsToken
);
} else {
throw new AuthenticationServiceException("Authentication method not supported: " + httpServletRequest
.getMethod());
}
}
}
provider
@Component
public class SmsProvider implements AuthenticationProvider {
@Autowired
private SelfUserDetailsService userDetailsService
;
@Autowired
private StringRedisTemplate redisTemplate
;
@Override
public Authentication
authenticate(Authentication authentication
) throws AuthenticationException
{
String mobile
= (String
) authentication
.getPrincipal();
String code
= (String
) authentication
.getCredentials();
String codeInRedis
= redisTemplate
.opsForValue().get(String
.join(":", Constant
.RedisKey
.PC_LOG_SMS
, mobile
));
if (StringUtils
.equals(codeInRedis
, code
)) {
UserDetails userDetails
= userDetailsService
.loadUserByUsername(mobile
);
return new SmsToken(userDetails
.getAuthorities(),userDetails
.getUsername(),userDetails
.getPassword());
}
throw new BadCredentialsException("验证码错误");
}
@Override
public boolean supports(Class
<?> authentication
) {
return SmsToken
.class.isAssignableFrom(authentication
);
}
}
token
public class SmsToken extends AbstractAuthenticationToken {
private String username
;
private String credentials
;
public SmsToken(String mobile
,String credentials
) {
super(null
);
this.username
= mobile
;
this.setAuthenticated(false);
this.credentials
= credentials
;
}
public SmsToken(Collection
<? extends GrantedAuthority> authorities
,String username
,String credentials
) {
super(authorities
);
this.username
= username
;
this.credentials
= credentials
;
this.setAuthenticated(true);
}
@Override
public String
getCredentials() {
return credentials
;
}
@Override
public String
getPrincipal() {
return username
;
}
}
配置类中为filter配置处理器
SmsAuthenticationFilter smsAuthenticationFilter
= new SmsAuthenticationFilter(Constant
.RedisKey
.PC_LOG_SMS
);
smsAuthenticationFilter
.setAuthenticationSuccessHandler(authenticationSuccessHandler
);
smsAuthenticationFilter
.setAuthenticationFailureHandler(authenticationFailureHandler
);
smsAuthenticationFilter
.setAuthenticationManager(authenticationManagerBean());
http
.addFilterBefore(smsAuthenticationFilter
,UsernamePasswordAuthenticationFilter
.class);