最近在写登录、注册接口,所以就写了这个demo来练练手,实际开发还是需要根据不同环境做调整
鄙人记性差,写这个正好回顾一下 ,网上找了很多资料,还是谢谢各路大神的指引
已经自测过,登录和权限认证都没问题
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
0.9.0
com.alibaba
fastjson
1.2.78
最基础的导包
连接数据库,配置jdbc,mybatisplus这些我就不多说了
首先网上抄个JWTUtil token生成工具,然后配置redis(用于存储token)这里就随意了
认证流程我就不走一遍了,有兴趣的网上搜一搜,贴点主要的
首先进入UsernamePasswordAuthenticationFilter
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
注意的是,这里是表单提交才能获取到username和password,我被坑了好久才找到原因
而且提交参数必须和这个一致
然后进入this.getAuthenticationManager().authenticate(authRequest);
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
for (AuthenticationProvider provider : getProviders()) {
.......篇幅太长就省略了
result = provider.authenticate(authentication);
return result;
}
}
}
这里交给了AuthenticationProvider的实现类 来处理
大概意思就是多种认证方式,只要有一个成功了,就放入Authentication中去
后面的手机号码登录、第三方登录会用到
第二个是不是特别很眼熟
AbstractUserDetailsAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider{
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
}
这是一个抽象方法,又交给它的子类(工具人)来干实事
DaoAuthenticationProvider
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
}
additionalAuthenticationChecks方法中两个参数UserDetails 是我们从数据库查询出来的用户数据
UsernamePasswordAuthenticationToken 是用户填写的时提交的账户密码,交给security封装好的
这里就比对一下密码是否一样,当然账户比对在前面已经做了。
那么首先就是创建一个UserDetails 来封装我们从数据库里查到的用户信息
public class LoginUser implements UserDetails {
//存储用户信息
private User user;
//存储 securityContext所需权限的集合
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
然后就是从哪里获取这些值,请注意
this.getUserDetailsService().loadUserByUsername(username);
所以需要实现UserDetailsService
@Service
public class UserDetailServiceImp implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("username:{}",username);
User user = userRepository.selectByUserName(username);
if(Objects.isNull(user))
throw new UsernameNotFoundException("用户名或密码错误");
LoginUser loginUser = new LoginUser();
loginUser.setUser(user);
return loginUser;
}
}
经过过滤链,如果认证成功,那么你可以在Security上下文中获取到用户信息
在自己的登录service中获取
//这里会在config配置里注入
@Autowired
private AuthenticationManager authenticationManager;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//如果不用表单提交,则可以用这种方式
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
//认证通过,生成一个token
LoginUser principal = (LoginUser) authentication.getPrincipal();
User user = principal.getUser();
String userId = user.getUserId();
String token = JWTUtil.createToken(userId);
//把user放入redis中
redisUtil.set(userId,user,60*60);
return JsonResult.success(token);
最后就是jwt过滤
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("base_token");
if(StringUtils.isEmpty(token)){
//如果token为空,放行,给security拦截,因为有可能是登录
filterChain.doFilter(request,response);
return;
}
//解析token
Claims claims = JWTUtil.parseToken(token);
String userId = claims.getSubject();
//从数据库获取到user信息
User user = (User)redisUtil.get(userId);
if(Objects.isNull(user)){
throw new UsernameNotFoundException("用户未登录");
}
//第三个参数是权限,后面鉴权会加上去
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request,response);
}
}
最后就是配置securityConfig
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private UserDetailServiceImp userDetailServiceImp;
@Bean
protected PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//放行登录接口 user/login anonymous()只能未登录的状态访问
http.authorizeRequests()
.antMatchers("/login").anonymous()
//permitAll() 所有人能访问(登录和未登录)
.antMatchers("/index").permitAll()
//除上述外所有请求都要认证
.and().authorizeRequests();
//基于表单提交的login会进入UsernamePasswordAuthenticationFilter
//http.formLogin().loginPage("/login");
//csrf 关闭session获取到securityContext
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//注销 并回到首页
http.logout().logoutUrl("/");
//记住我功能
http.rememberMe();
//把自定义的jwt过滤器 放在 UsernamePasswordAuthenticationFilter 之前执行
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
http.cors();
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailServiceImp);
}
@Override
public void configure(WebSecurity web) throws Exception {
//放行所需要用到的静态资源,允许访问 swagger相关
web.ignoring().antMatchers( "/swagger-ui.html",
"/swagger-ui/*",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/webjars/**");
}
}
这里使用的BCryptPasswordEncoder,所以在设置密码的时候也要用这个来编码
下次有空再把授权补上吧