md5utils中解密方法_SpringSecurity在前后端分离项目中自定义登录及源码分析

课纲

  • 自定义登录流程
  • 源码分析
  • 常见问题总结

登录功能

背景

项目登录功能在没有使用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接口实现类之一。

b0ea3a3c706e290977cf9e33a2b5a7c0.png

step2:调用AuthenticationManager的authenticate()认证

Authentication authentication=  authenticationManager.authenticate(usernamePasswordAuthenticationToken);
faa052b6de30488adb78a04aff24f874.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);}
6e79fe06b13c5daba908fe421b50638f.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());    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值