课纲
- 自定义登录流程
- 源码分析
- 常见问题总结
登录功能
背景
项目登录功能在没有使用SpringSecurity之前,流程如下:
- 用户输入用户名和密码并提交到Controller
- 在Controller层调用service层校验是否正确
- 如果正确,将用户信息写入session
- 返回用户的json数据到前端
存在的问题
- 不安全,可能会造成xss、csrf、session伪造攻击等。
为了解决上述安全问题,引入SpringSecurity。
配置SpringSecurity环境
引入依赖
org.springframework.bootspring-boot-starter-security
自定义SpringSecurityConfig配置类-完整版
@Configuration@EnableWebSecuritypublic class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public AuthenticationEntryPoint authenticationEntryPoint(){ return new MyLoginAuthenticationEntryPoint(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/","/portal/user/login.do","/portal/product/list.do").permitAll() .anyRequest().authenticated() .and() .logout().permitAll() .and() .formLogin() .and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint()) ; http.csrf().disable();//关闭csrf } @Autowired UserService userService; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(new MyPasswordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**","/css/**","/images/**"); } }
SpringSecurity集成登录功能需解决的问题
问题一: SpringSecurity如何往前端返回Json数据
用户未登录或者登录认证失败情况下,如何返回json数据?
在上面SpringSecurityConfig类配置AuthenticationEntryPoint:
@Bean public AuthenticationEntryPoint authenticationEntryPoint(){ return new MyLoginAuthenticationEntryPoint(); }
MyLoginAuthenticationEntryPoint类的实现:
public class MyLoginAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { try { response.reset(); response.addHeader("Content-Type","application/json;charset=utf-8"); PrintWriter printWriter=response.getWriter(); ServerResponse serverResponse=ServerResponse.createServerResponseByFail(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getMsg()); ObjectMapper objectMapper=new ObjectMapper(); String info=objectMapper.writeValueAsString(serverResponse); printWriter.write(info); printWriter.flush(); printWriter.close(); } catch (IOException e) { e.printStackTrace(); } }}
问题二:SpringSecurity登录验证机制如何应用在项目中
查询SpringSecurity官方文档,需要通过Authentication、AuthenticationManager、SecurityContextHolder、SecurityContext、UserDetailsService等接口完成自定义登录验证机制。
SpringSecurity认证机制流程:
- 组装认证用户信息Token
- 使用用户信息Token完成SpringSecurity认证获得认证对象Autehntication。
- 设置第二步骤中的认证对象设置为当前的SpringSecurity的认证对象。
- 返回前端数据。
SpringSecurityCofig配置类中配置AuthenticatinManager
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Autowired AuthenticationManager authenticationManager; public ServerResponse login(String username, String password, HttpSession session, HttpServletRequest request){ ServerResponse serverResponse= userService.loginLogic(username, password); if(serverResponse.isSucess()){ // 用户登录成功后,调用SpringSecurity认证机制 //生成认证token ,密码MD5加密 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken= new UsernamePasswordAuthenticationToken(username, MD5Utils.getMD5Code(password)); //用户信息保存到details字段中 usernamePasswordAuthenticationToken.setDetails(serverResponse.getData()); //调用AuthenticationManager的authenticate方法进行认证 Authentication authentication=authenticationManager.authenticate(usernamePasswordAuthenticationToken); //将认证对象设置为SecurityContext的认证对象 SecurityContextHolder.getContext().setAuthentication(authentication); //将securityContext保存到session中 session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); } return serverResponse; }
用户认证源码分析
step1:创建用户认证token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken= new UsernamePasswordAuthenticationToken(username, MD5Utils.getMD5Code(password));
UsernamePasswordAuthenticationToken是Authenticateion接口实现类之一。
![9acdf70e1c423760e08f99e86e88164d.png](https://img-blog.csdnimg.cn/img_convert/9acdf70e1c423760e08f99e86e88164d.png)
step2:调用AuthenticationManager的authenticate()认证
Authentication authentication= authenticationManager.authenticate(usernamePasswordAuthenticationToken);
![ccfbacd777f05655e1e0353809a650e4.png](https://img-blog.csdnimg.cn/img_convert/ccfbacd777f05655e1e0353809a650e4.png)
AuthenticationManager
AuthenticationManager是一个接口,是认证方法的入口,接收一个Authentication对象作为参数
public interface AuthenticationManager {Authentication authenticate(Authentication authentication)throws AuthenticationException;}
ProviderManager
它是AuthenticationManager的一个实现类,实现了authenticate(Authentication authentication)方法,还有一个成员变量
List providers
public class ProviderManager implements AuthenticationManager, MessageSourceAware,InitializingBean {...private List providers = Collections.emptyList();...public ProviderManager(List providers) {this(providers, null);}}
AuthenticationProvider
AuthenticationProvider也是一个接口,包含两个函数authenticate和supports。当Spring Security默认提供的Provider不能满足需求的时候,可以通过实现AuthenticationProvider接口来扩展出不同的认证提供者
public interface AuthenticationProvider { //通过参数Authentication对象,进行认证 Authentication authenticate(Authentication authentication) throws AuthenticationException; //是否支持该认证类型 boolean supports(Class> authentication);}
![36ce0fee579f0a1cae9785107f5e3d5d.png](https://img-blog.csdnimg.cn/img_convert/36ce0fee579f0a1cae9785107f5e3d5d.png)
Authentication
Authentication是一个接口,通过该接口可以获得用户相关信息、安全实体的标识以及认证请求的上下文信息等
在Spring Security中,有很多Authentication的实现类。如UsernamePasswordAuthenticationToken、AnonymousAuthenticationToken和 RememberMeAuthenticationToken等等
通常不会被扩展,除非是为了支持某种特定类型的认证
public interface Authentication extends Principal, Serializable { //权限结合,可使用AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_ADMIN")返回字符串权限集合 Collection extends GrantedAuthority> getAuthorities(); //用户名密码认证时可以理解为密码 Object getCredentials(); //认证时包含的一些信息。如remoteAddress、sessionId Object getDetails(); //用户名密码认证时可理解时用户名 Object getPrincipal(); //是否被认证,认证为true boolean isAuthenticated(); //设置是否被认证 void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;}
UserDetails
UserDetails也是一个接口,主要封装用户名密码是否过期、是否可用等信息
public interface UserDetails extends Serializable { //权限集合 Collection extends GrantedAuthority> getAuthorities(); //密码 String getPassword(); //用户名 String getUsername(); //用户名是否没有过期 boolean isAccountNonExpired(); //用户名是否没有锁定 boolean isAccountNonLocked(); //用户密码是否没有过期 boolean isCredentialsNonExpired(); //账号是否可用(可理解为是否删除) boolean isEnabled(); }
具体认证过程
ProviderManager
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //获取当前的Authentication的认证类型 Class extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); //遍历所有的providers for (AuthenticationProvider provider : getProviders()) { //判断该provider是否支持当前的认证类型。不支持,遍历下一个 if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { //调用provider的authenticat方法认证 result = provider.authenticate(authentication); if (result != null) { //认证通过的话,将认证结果的details赋值到当前认证对象authentication。然后跳出循环 copyDetails(authentication, result); break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } ...... }
AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider是 AuthenticationProvider 的核心实现类
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //如果authentication不是UsernamePasswordAuthenticationToken类型,则抛出异常 Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); // 获取用户名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); //从缓存中获取UserDetails boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); //缓存中没有,则从子类DaoAuthenticationProvider中获取 if (user == null) { cacheWasUsed = false; try { //获取用户信息。由子类DaoAuthenticationProvider实现 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } ...... } try { //前检查。由DefaultPreAuthenticationChecks实现(主要判断当前用户是否锁定,过期,冻结User) preAuthenticationChecks.check(user); //附加检查。由子类DaoAuthenticationProvider实现 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { ...... } //后检查。由DefaultPostAuthenticationChecks实现(检测密码是否过期) postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //将已通过验证的用户信息封装成 UsernamePasswordAuthenticationToken 对象并返回 return createSuccessAuthentication(principalToReturn, authentication, user); }
1、前检查和后检查的参数为UserDetails,正好对应UserDetails中的4个isXXX方法
2、retrieveUser()和additionalAuthenticationChecks()由子类DaoAuthenticationProvider实现
3、createSuccessAuthentication如下:
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { //重新封装成UsernamePasswordAuthenticationToken。包含用户名、密码,以及对应的权限 //该构造方法会给父类Authentication赋值: super.setAuthenticated(true) UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; }
DaoAuthenticationProvider
DaoAuthenticationProvider实现了父类的retrieveUser()和additionalAuthenticationChecks()方法
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { UserDetails loadedUser; try { //调用UserDetailsService接口的loadUserByUsername获取用户信息 //通过实现UserDetailsService接口来扩展对用户密码的校验 loadedUser = this.getUserDetailsService().loadUserByUsername(username); } //如果找不到该用户,则抛出异常 if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; }
@SuppressWarnings("deprecation") protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { Object salt = null; if (this.saltSource != null) { salt = this.saltSource.getSalt(userDetails); } //密码为空,则直接抛出异常 if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } //获取用户输入的密码 String presentedPassword = authentication.getCredentials().toString(); //将缓存中的密码(也可能是自定义查询的密码)与用户输入密码匹配 //如果匹配不上,则抛出异常 if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
UserDetailsService
public interface IUserService extends UserDetailsService {}
@Servicepublic class UserService implements IUserService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user=userMapper.findUserByUsername(username); if(user!=null){ //创建角色集合对象 Collection authorities = new ArrayList<>(); GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_USER"); authorities.add(grantedAuthority); org.springframework.security.core.userdetails.User user1 = new org.springframework.security.core.userdetails.User (user.getUsername(),user.getPassword(), authorities); return user1; } return null; } }
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
因为Spring boot 2.x引用的security 依赖是 spring security 5.X版本,此版本需要提供一个PasswordEncorder的实例,否则后台汇报错误:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
并且页面毫无响应。
解决方案: 创建PasswordEncorder的实现类
public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { //采用md5编码 return MD5Utils.getMD5Code((String )rawPassword); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals((String) rawPassword); }}
在springSecuritycofing配置类中注册MyPasswordEncoder
@Autowired UserService userService; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(new MyPasswordEncoder()); }