调用登录_SpringSecurity在前后端分离项目中自定义登录及源码分析

news/2024/7/8 2:25:59

课纲

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

登录功能

背景

项目登录功能在没有使用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

step2:调用AuthenticationManager的authenticate()认证

Authentication authentication=  authenticationManager.authenticate(usernamePasswordAuthenticationToken);
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

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());    }

http://www.niftyadmin.cn/n/4334229.html

相关文章

es6新的方法

var o {a:1}Object.keys(o); //返回对象自身的所有可枚举的属性的键名Object.assign() //忽略不可枚举的属性&#xff0c;只拷贝对象自身的可枚举属性Array.from() //将类似数组的对象&#xff0c;不管对象有没有部署iterator接口&#xff0c;转化为真正的数组[...arrayLik…

从远程主机断开连接_在外的同事们,远程连接不进公司的服务器,我拿什么来拯救你?...

我们公司大概有五个业务员是比较自由的&#xff0c;平时不是在日本就是在美国&#xff0c;要么就是在飞机和火车上&#xff0c;世界之大&#xff0c;哪里都有她们的去处。为了他们&#xff0c;我们专门设置了一台远程服务器供她们使用&#xff0c;主要的功能是通过远程进来使用…

P1158 导弹拦截

P1158 导弹拦截 思路&#xff1a; 按每个点到第一个系统的距离排序&#xff0c;然后预处理出每个点及其之后的点到第二个系统的距离的最大值&#xff0c;再循环一遍枚举答案。 代码&#xff1a; 1 #include <cstdio>2 #include <iostream>3 #include <algorithm…

前端png转pdf_有备无患,轻量在线PDF工具,能上网就能用

正式办公环境下&#xff0c;大家可能有 Adobe / MS Office / Foxit 之类一堆PDF工具可用。 不过有时出差在外&#xff0c;面对一台新的 PC/Mac&#xff0c;或者手机上临时要处理一下 PDF。要么时间紧迫来不及&#xff0c;要么连安装软件的权力也未必有&#xff0c;这时往往比较…

用自己的电脑搭云服务器吗_用别人的芯片,做自己的X86服务器:国内浪潮第一,华为第二...

众所周知&#xff0c;在PC领域&#xff0c;X86架构的芯片是一家独大的&#xff0c;虽然目前也有一些厂商想要向X86架构发起挑战&#xff0c;比如华为去年推出了鲲鹏主板&#xff0c;想将鲲鹏920用于PC&#xff0c;但就目前来讲&#xff0c;还需要很长时间。所以一直以来&#x…

怎么定义int_Java方法到底怎么写!快看我!

Snoopy 煜钊读完需要7分钟速读仅需 3 分钟在学习 Java 的过程中&#xff0c;我们经常会对方法的设计有些迷茫&#xff0c;怎么取名&#xff0c;怎么传参&#xff0c;怎么得到返回值。甚至有的小伙伴并不懂上面这些术语&#xff0c;用你们的话来讲&#xff1a;诶&#xff0c;我这…

python 获取文件编码格式_python 读写文件包含多种编码格式的解决方式

今天写一个脚本文件&#xff0c;需要将多个文件中的内容汇总到一个txt文件中&#xff0c;由于多个文件有三种不同的编码方式&#xff0c;读写出现错误&#xff0c;先将解决方法记录如下&#xff1a;# -*- coding: utf-8 -*-import waveimport pylab as plimport numpy as npimp…

es6比较两个一样的对象_前端框架必学技能-ES6之class

传统的 JavaScript 中只有对象&#xff0c;没有类的概念。它是基于原型的面向对象语言。原型对象特点就是将自身的属性共享给新对象。这样的写法相对于其它传统面向对象语言来讲&#xff0c;很有一种独树一帜的“感脚”&#xff01;非常容易让人困惑&#xff01;首先大家要明白…