前言
搜索了网上的很多资料,主要参考的是这篇,推荐给大家。
https://www.cnblogs.com/xifengxiaoma/p/11106220.html
这个很早就写了,但是觉得写的不好就一直没发。今天看到有人问相关问题了,想了想,可以先放出来,后续如果要修改的话再继续。
spring security 用来做登录认证和授权的。
如上图,简单的讲就是 请求 经过一系列的 filter , 到达api接口, 然后返回接口处理后的结果 。
其中,绿色的部门UsernamePasswordAuthenticationFilter 和 BasicAuthenticationFilter这些是可配置是否生效的,其它的不可以控制。
配置文件概览
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用自定义登录身份认证组件
auth.authenticationProvider(jwtAuthenticationProvider());
//user: admin password: admin
auth.inMemoryAuthentication()
.withUser("admin").roles("admin").password("$2a$10$zitnDs8T9qWXn4XjstH/PuOigjid.YMWDeyuONypBLZwwBDuHWlhe");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
http.cors().and().csrf().disable()
//基于token,所以不需要session,也是默认配置
//.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 登录URL
.antMatchers("/signIn").permitAll()
.antMatchers("/signUp").permitAll()
// swagger
.antMatchers("/swagger**/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/v3/**").permitAll()
// 其他所有请求需要身份认证
.anyRequest().authenticated();
//不重写登录认证的话,可以直接
//.adn().formLogin();
//同理,开启中间的filter
//.and().httpBasic();
// 开启登录认证流程过滤器
http.addFilterBefore(jwtLoginFilter(), UsernamePasswordAuthenticationFilter.class);
// 访问控制时登录状态检查过滤器
//http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
// 退出登录处理器
http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
JwtLoginFilter jwtLoginFilter() throws Exception {
JwtLoginFilter jwtLoginFilter = new JwtLoginFilter();
jwtLoginFilter.setAuthenticationManager(authenticationManager());
return jwtLoginFilter;
}
@Bean
JwtAuthenticationProvider jwtAuthenticationProvider() throws Exception {
JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider();
jwtAuthenticationProvider.setUserDetailsService(userDetailsService);
jwtAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return jwtAuthenticationProvider;
}
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
}
登录认证有两种处理方式,一种是采用拦截登录接口的方式,
另一种是 在controller里面直接认证,然后将认证结果放入SecurityContext。
先讲下第一种方式:
理清下流程:filter 拦截接口 --> provider 验证信息是否正确 (调用retrieveUser方法去获取存储的信息) --> 返回校验结果。
UsernamePasswordAuthenticationFilter 是第一步的默认实现,拦截 /login 接口,获取name,password参数并封装成AuthenticationToken去进行下一步校验的。
如果使用默认的login接口,可以直接继承UsernamePasswordAuthenticationFilter类,并重写里面的attemptAuthentication方法。
如果使用其他接口,需要继承它的父类AbstractAuthenticationProcessingFilter类,照着写就可以的。
一. 新建JwtLoginFilter类
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtLoginFilter.class);
public JwtLoginFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = request.getParameter("username");
String password = request.getParameter("password");
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//这里是继承的UsernamePasswordAuthenticationToken
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(username, password);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
return getAuthenticationManager().authenticate(authRequest);
}
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Authentication request failed: " + failed.toString(), failed);
}
getRememberMeServices().loginFail(request, response);
CustomizeResponse customizeResponse = new CustomizeResponse();
customizeResponse.setCode(ResponseConstants.CODE_401);
customizeResponse.setMessage(ResponseConstants.MSG_401);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(customizeResponse));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 存储登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authResult);
// 记住我服务
getRememberMeServices().loginSuccess(request, response, authResult);
// 触发事件监听器
if (eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
//生成token,返回
String jwtToken = JwtTokenUtils.generateToken(authResult);
CustomizeResponse customizeResponse = new CustomizeResponse();
customizeResponse.setCode(ResponseConstants.CODE_200);
customizeResponse.setMessage(ResponseConstants.MSG_200);
customizeResponse.setData(jwtToken);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(customizeResponse));
}
/**
* json形式的post请求,需要从流中读取参数 。获取请求Body
* @param request
* @return
*/
public String getBody(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 7303024501909676990L;
private String token;
public JwtAuthenticationToken(Object principal, Object credentials){
super(principal, credentials);
}
public JwtAuthenticationToken(Object principal, Object credentials, String token){
super(principal, credentials);
this.token = token;
}
public JwtAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
super(principal, credentials, authorities);
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
第一步其实就只是通过filter拦截接口,获取参数并封装,然后调用后面的认证方法。
第二步是比较复杂的,可以看到第一步attemptAuthentication方法最后调用了authenticate方法。每个provider通过supports来表明自己是支持哪种校验的。
DaoAuthenticationProvider 是第二步的默认实现,我们可以点进它继承的AbstractUserDetailsAuthenticationProvider进去查看 authenticate 方法的具体流程,同时也可以在父类里搜到它的supports,很明显是支持我们第一步封装的JwtAuthenticationToken的。
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
我们这里直接继承DaoAuthenticationProvider去进行重写,不继承父类了。差别在于,父类只提供了抽象方法 retrieveUser, 而子类有实现,是调用UserDetailsService().loadUserByUsername() 方法,这个我们放到第三步去介绍。
在认证方法 authenticate 里面,流程是
- 获取user (这里包括通过retrieveUser去获取用户信息)
- 2.1 preAuthenticationChecks.check() 默认实现是检查账号是否有效,过期,锁定
2.2 additionalAuthenticationChecks 这里进行密码检查 这两步如果报错会重试一次 - postAuthenticationChecks.check 这里默认实现是检查密码是否过期。
二. 新建JwtAuthenticationProvider类 这里我就只重写了additionalAuthenticationChecks方法
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtLoginFilter.class);
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
LOGGER.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("additionalAuthenticationChecks.badCredentials", "Bad credentials"));
}
// 验证密码是否匹配
String password = authentication.getCredentials().toString();
//再次加密后与数据库的进行比对
password = SM4Util.encryptCbcDataTimes(password, null, null);
if(!password.equals(userDetails.getPassword())){
LOGGER.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage("additionalAuthenticationChecks.badCredentials", "Bad credentials"));
}
}
}
ps: 获取用户信息,即实现loadUserByUsername方法,往上翻到配置项的代码处,我们在注解bean,jwtAuthenticationProvider类的时候,向里面注入了一个我们自定义的userDetailsService,示例代码如下:
@Service
public class CustomUserDetailService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Override
public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
UserDAO userDAO = userMapper.queryUserByLoginName(loginName);
if(null == userDAO){
throw new UsernameNotFoundException("User " + loginName + " was not found");
}
// 2. 设置角色
List<String> roles = userRoleMapper.queryUserRole(userDAO.getUserId());
Collection<GrantedAuthority> grantedAuthorities = AuthorityUtils.createAuthorityList(roles.toArray(new String[0]));
//Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("admin");
//grantedAuthorities.add(grantedAuthority);
return new User(userDAO.getUserName(), userDAO.getPassword(), grantedAuthorities);
}
}
第三步,返回结果。这个也是在前面第一步filter里面重写了的。成功或失败分别进行一些处理并放回。
这样第一种方式,拦截请求就结束了。第二种直接调请求接口的,思想同这个是一样的,就不赘述了。至于登录以外的其它请求,则可以新建filter,获取request里面的携带的之前登录认证返回的token,后续进行相应处理即可。