Spring Security 使用自带的 formLogin

Session、Cookie 登陆认证

就是认证是否为合法用户,简单的说是登录。一般为匹对用户名和密码,即认证成功。

在 Spring Security 认证中,只要解决如下几个问题:

  • 哪个类表示用户?
  • 哪个属性表示用户名?
  • 哪个属性表示密码?
  • 怎么通过用户名取到对应的用户?
  • 密码的验证方式是什么?

所有的自定义行为都是围绕这几个问题展开的

认证的执行流程就是

  1. 它会拿到用户输入的用户名密码;
  2. 根据用户名通过 UserDetailsServiceloadUserByUsername(username) 方法获得一个用户对象;
  3. 获得一个 UserDetails 对象,获得内部的成员属性 password
  4. 通过 PasswordEncodermatchs(s1, s2) 方法对比用户的输入的密码和第3步的密码;
  5. 匹配成功;

获取用户名和密码

Spring Boot 整合 Spring Security(前后端分离时的json登录方式,解决获取不到用户名密码问题)
springboot+security整合2

默认的账户名和密码的参数名分别是 usernamepassword 可以自定义账户和密码的参数名

http
.formLogin()
.usernameParameter("my_username")
.passwordParameter("my_password")

如果是自己验证用户名密码的话,Spring Security 仅仅支持传统的 form 表单方式(form-data)登录。这是一个比较大的坑点。现在都流行使用前后端分离,前端发送的是 json 格式数据。所以需要自己定制 UsernamePasswordAuthenticationFilter 这个类

获取用户名密码是在 UsernamePasswordAuthenticationFilter 这个类里面的 attemptAuthentication 方法,如下

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
 
		String username = obtainUsername(request);
		String password = obtainPassword(request);
 
		if (username == null) {
			username = "";
		}
 
		if (password == null) {
			password = "";
		}
 
		username = username.trim();
 
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
 
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
 
		return this.getAuthenticationManager().authenticate(authRequest);
	}

再进一步可以看到获取用户名密码的方法

// 这个 passwordParameter 为 password
// 同理 usernameParameter 为 username
protected String obtainPassword(HttpServletRequest request) {
	return request.getParameter(passwordParameter);
}

所以如果要从 JSON 里取得密码和用户名,需要继承这个 UsernamePasswordAuthenticationFilter 类,重写 attemptAuthentication 方法(例如添加验证码之类的操作也是在这里入手)

例如这里增加一个验证码操作

public class MyUsernamePasswordAuthentication extends UsernamePasswordAuthenticationFilter{
    
    private Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        //我们可以在这里进行额外的验证,如果验证失败抛出继承AuthenticationException的自定义错误。
        log.info("在这里进行验证码判断");
        //只要最终的验证是账号密码形式就无需修改后续过程
        return super.attemptAuthentication(request, response);
    }

    @Override
    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        // TODO Auto-generated method stub
        super.setAuthenticationManager(authenticationManager);
    }
}

将自定义登录配置到 Security 中

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    .csrf() // 跨站
    .disable() // 关闭跨站检测
    // 自定义鉴权过程,无需下面设置
    // 验证策略
    .authorizeRequests()
        // 无需验证路径
        .antMatchers("/public/**").permitAll()
       .antMatchers("/user/**").permitAll()
       // 放行登录
       .antMatchers("/login").permitAll()
       .antMatchers(HttpMethod.GET, "/user").hasAuthority("getAllUser") // 拥有权限才可访问
        // 拥有任一权限即可访问
        .antMatchers(HttpMethod.GET, "/user").hasAnyAuthority("1","2")
        // 角色类似,hasRole(),hasAnyRole()
        .anyRequest().authenticated()
    .and()
    // 自定义异常处理
    .exceptionHandling()
        .authenticationEntryPoint(myAuthenticationEntryPoint) // 未登录处理
        .accessDeniedHandler(myAccessDeniedHandler)//权限不足处理
    .and()
    // 加入自定义登录校验
    .addFilterBefore(myUsernamePasswordAuthentication(),UsernamePasswordAuthenticationFilter.class)
    // 默认放在内存中
    .rememberMe()
        .rememberMeServices(rememberMeServices())
        .key("INTERNAL_SECRET_KEY")
//       重写 usernamepasswordauthenticationFilter 后,下面的formLogin()设置将失效,需要手动设置到个性化过滤器中
//        .and()
//      .formLogin()
//          .loginPage("/public/unlogin") //未登录跳转页面,设置了authenticationentrypoint后无需设置未登录跳转面
//          .loginProcessingUrl("/public/login")//登录api
//            .successForwardUrl("/success")
//            .failureForwardUrl("/failed")
//            .usernameParameter("id")
//            .passwordParameter("password")
//          .failureHandler(myAuthFailedHandle) //登录失败处理
//          .successHandler(myAuthSuccessHandle)//登录成功处理
//            .usernameParameter("id")
    .and()
    .logout()//自定义登出
        .logoutUrl("/public/logout")
        .logoutSuccessUrl("public/logoutSuccess")
        .logoutSuccessHandler(myLogoutSuccessHandle);
}

// 然后再编写Bean,代码如下:
@Bean
public MyUsernamePasswordAuthentication myUsernamePasswordAuthentication(){
    MyUsernamePasswordAuthentication myUsernamePasswordAuthentication = new MyUsernamePasswordAuthentication();
    myUsernamePasswordAuthentication.setAuthenticationFailureHandler(myAuthFailedHandle); //设置登录失败处理类
    myUsernamePasswordAuthentication.setAuthenticationSuccessHandler(myAuthSuccessHandle);//设置登录成功处理类
    myUsernamePasswordAuthentication.setFilterProcessesUrl("/public/login");
    myUsernamePasswordAuthentication.setRememberMeServices(rememberMeServices()); //设置记住我
    myUsernamePasswordAuthentication.setUsernameParameter("id");
    myUsernamePasswordAuthentication.setPasswordParameter("password");
    return myUsernamePasswordAuthentication;
}

UserDetails

参考资料 Spring Security自定义用户认证

这个接口就是下面的 UserDetailsService 的返回值,虽然 Spring Security 自带的实现类 org.springframework.security.core.userdetails.User 已经够强大了,但是还有有必要去了解这个接口,以便后续自定义

该对象也是一个接口,包含一些用于描述用户信息的方法,源码如下:

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}
  • getAuthorities 获取用户包含的权限,返回权限集合,权限是一个继承了 GrantedAuthority 的对象;
  • getPasswordgetUsername 用于获取密码和用户名;
  • isAccountNonExpired 方法返回 boolean 类型,用于判断账户是否未过期,未过期返回 true 反之返回 false
  • isAccountNonLocked 方法用于判断账户是否未锁定;
  • isCredentialsNonExpired 用于判断用户凭证是否没过期,即密码是否未过期;
  • isEnabled 方法用于判断用户是否可用。

UserDetailsService

怎么通过用户名取到对应的用户?

只需要去实现这个 UserDetailsService 接口,里面有个 loadUserByUsername 方法就是用来找到用户的

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

在这个 loadUserByUsername(String username) 里面实现登陆逻辑,如果未找到用户可以抛出一个 UsernameNotFoundException 异常,返回值是一个 UserDetails 接口的实现类,默认会使用 Spring Security 定义的 User

实现 UserDetailsService 接口例

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    // 懒得连接数据库,这里直接使用 Map 替代
    private static final Map<String, String> dates;

    static {
        dates = new HashMap<>();
        // username, password
        dates.put("admin", "admin");
    }

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String rawPassword = dates.get(username);
        // 这里模拟去数据库查询
        if (rawPassword == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        // 匹配成功则用其生成加密后的密文传入 UserDetails
        String password = passwordEncoder.encode(rawPassword);
        // 这个 commaSeparatedStringToAuthorityList 就是把输入的字符串根据逗号分割成 List<GrantedAuthority>
        return new User(username, password, AuthorityUtils
                .commaSeparatedStringToAuthorityList("admin,normal"));
    }
}

这里 User 对象返回值的第三个参数实际上就是权限列表 Collection<? extends GrantedAuthority> authorities

这里通过 AuthorityUtils.commaSeparatedStringToAuthorityList() 这个工具类将权限转换成 GrantedAuthority 集合(使用 , 分割不同权限)

比对权限是否存在的方式

@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
    // 获取主体
    Object principal = authentication.getPrincipal();
    log.info(request.getRequestURI());
    // 判断主体是否属于 UserDetails
    if (principal instanceof UserDetails) {
        UserDetails userDetails = (UserDetails) principal;
        // 获取权限列表
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        // 判断请求的 URI 是否在权限里
        return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
    }
    return false;
}

PasswordEncoder

loadUserByUsername 这个方法可以看到只有一个 username 参数,那密码在哪里比较呢? 密码的比较使用的是 PasswordEncoder;它也是一个接口

public interface PasswordEncoder {
    // 加密密码
	String encode(CharSequence rawPassword);
    // 对密码进行比对
	boolean matches(CharSequence rawPassword, String encodedPassword);
    // 对已经加密的密码再次加密
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}

不过一般使用自带的实现类 BCryptPasswordEncoder 使用例

@Test
void testPasswordEncoder() {
    PasswordEncoder pw = new BCryptPasswordEncoder();
    // 加密密码
    // 注意 这个 BCrypt加密算法每次加密得到的密文都是不一样的,所以就算是一样的密码两次加密都不会相同
    String encode = pw.encode("12345678");
    log.info(encode);
    // 比对密码
    boolean matches = pw.matches("12345678", encode);
    log.info(String.valueOf(matches));
}

/** 输出如下
 * $2a$10$28sxxWLV85qKIIGK4mR9YuM/JjBGotUnaX8WROHjDV1IcLsmXIhOG
 * true
 */

登陆成功处理器

就是去实现 AuthenticationSuccessHandler 接口

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        RespBean ok = RespBean.ok("登录成功!", authentication.getPrincipal());
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(ok));
        out.flush();
        out.close();
    }
}

然后再在配置里使用自定义的处理器就行了

http
...
.successHandler(new MyAuthenticationSuccessHandler())

登陆失败处理器

去实现 AuthenticationFailureHandler 接口

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        RespBean error = RespBean.error("登录失败");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(error));
        out.flush();
        out.close();
    }
}

然后再在配置里使用自定义的处理器

http
...
.failureHandler(new MyAuthenticationFailureHandler())

自定义 403 处理

去实现 AccessDeniedHandler 接口

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        RespBean error = RespBean.error("权限不足,访问失败");
        response.setStatus(403);
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out = response.getWriter();
        out.write(new ObjectMapper().writeValueAsString(error));
        out.flush();
        out.close();
    }
}

除了上面那种高度自定义的写法还可以像这样直接调用方法

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.sendError(403, "权限不足,访问失败");
}

注册这个 Handle

http
.exceptionHandling()
                // 这个 Http403ForbiddenEntryPoint 是自带的实现类
                .authenticationEntryPoint(new Http403ForbiddenEntryPoint()) // 未登陆的请求处理
                .accessDeniedHandler(new MyAccessDeniedHandler()) // 未授权的请求处理

如何判断用户状态的?

用户登陆后 Spring Security 是怎么知道用户已经登陆过了呢?答案就是 Cookie 和 Session,用户登陆后服务端会返回一个 Cookie

JSESSIONID=41AEFF280BB9BF67CC23E9B9C740B075;

因为每次请求都会带上 Cookie,所以 Spring Security 能基于此完成对用户权限的鉴别

注意:如果使用的是 Ajax 跨域请求,需要配置一下,否则默认是不携带 Cookie 的

这里只讲 axios 的设置,后端的设置参看跨域请求那一篇文章

// 因为默认 Spring Security 是通过 Cookie 和 Session 来验证身份的,所以需要配置携带 Cookie
axios.interceptors.request.use(config => {
  config.withCredentials = true;
  return config;
});

自定义登陆页

默认 Spring Security 有一个自带的登陆界面,其账户名是 user 密码会在控制台打印出来,用户访问 /login 路径会跳转到这个登陆页,虽然集成了登陆页挺好的,但是一般都是需要各种客制化,所以还是需要掌握如何自定义登陆

前端分离登陆

参考资料 Spring Security登录使用JSON格式数据
参考资料 spring security简单教程以及实现完全前后端分离

现在大部分前后端分离的 Web 程序,尤其是前端普遍使用 Ajax 请求时,Spring Security 自带的登录系统就有一些不满足需求了。

因为 Spring Security 有自己默认的登录页,自己默认的登录控制器。而登录成功或失败,都会返回一个 302 跳转。登录成功跳转到主页,失败跳转到登录页。如果未认证直接访问也会跳转到登录页。但是如果前端使用 Ajax 请求,Ajax 是无法处理 302 请求的。前后端分离 Web 中,规范是使用 Json 交互。我们希望登录成功或者失败都会返回一个 Json。

注:登录接口和登录页面的区别,登录页面就是浏览器展示出来的页面;登录接口则是提交登录数据的地方,就是登录页面里边的 form 表单的 action 属性对应的值。

在 Spring Security 中,如果我们不做任何配置,默认的登录页面和登录接口的地址都是 /login,也就是说,默认会存在如下两个请求:

GET http://localhost:8080/login
POST http://localhost:8080/login
  • loginProcessingUrl:这个表示配置处理登录请求的接口地址,例如你是表单登录,那么 form 表单中 action 的值就是这里填的值。
  • loginPage:这个表示登录页的地址,例如当你访问一个需要登录后才能访问的资源时,系统就会自动给你通过重定向跳转到这个页面上来。(但是这个是前后端不分家时才使用的东西)

可以自定义 AuthenticationSuccessHandlerAuthenticationFailureHandlerAccessDeniedHandler 来返回不同的 JSON

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private UserDetailServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义的 UserDetailService
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 验证策略
                .authorizeRequests()
                // 放行登录
                .antMatchers(HttpMethod.POST,"/doLogin").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/doLogin")
                // 设置登陆成功后的处理(这个 MyAuthenticationSuccessHandler 是之前自定义的处理器,下面的同理)
                .successHandler(new MyAuthenticationSuccessHandler())
                // 设置登陆失败后的处理
                .failureHandler(new MyAuthenticationFailureHandler())
                .and()
                .csrf()// 要关掉这个 csrf
                .disable()
                .exceptionHandling()
                .authenticationEntryPoint(new Http403ForbiddenEntryPoint()) // 未登陆直接请求资源的处理(这里设置为返回 403)
                .accessDeniedHandler(new MyAccessDeniedHandler())
                .and().cors();

    }
}

注意这里的坑点!!csrf().disable() 这里的 csrf 一定要关掉,否则会一直显示 “权限不足,访问失败”。在 Security 的默认拦截器里,默认会开启 CSRF 处理,判断请求是否携带了 token,如果没有就拒绝访问。

不要将 CORS(跨站资源共享) 和 CSRF(跨站请求伪造)弄混

  • CORS(跨站资源共享) 是局部打破同源策略的限制,使在一定规则下 HTTP 请求可以突破浏览器限制,实现跨站访问。
  • CSRF 是一种网络攻击方式,也可以说是一种安全漏洞,这种安全漏洞在 web 开发中广泛存在。

退出登陆

http
  .logout()
  // 默认是 /logout
  .logoutUrl("/doLogout")
  //退出成功,返回json
  .logoutSuccessHandler((request,response,authentication) -> {
      RespBean ok = RespBean.ok("退出成功!", authentication.getPrincipal());
      response.setContentType("application/json;charset=utf-8");
      PrintWriter out = response.getWriter();
      out.write(new ObjectMapper().writeValueAsString(ok));
      out.flush();
      out.close();
  }).permitAll();

配置测试环境

使用 Vue 搭建一个测试环境

<body>
  <div id="app">
    <div>
      <span>username:</span>
      <input type="text" v-model='username'>
    </div>
    <div>
      <span>password:</span>
      <input type="text" v-model='password'>
    </div>
    <button @click='sub'>提交</button>
    <br>
    <br>
    <!-- 登陆成功后再访问这个 api -->
    <button @click="sayHello()">测试访问需要权限的 api</button>
    <br>
    <br>
    <button @click="logout()">退出登陆</button>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script src="https://cdn.bootcss.com/qs/6.5.1/qs.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    const qs = Qs // 引入QS库把请求参数转成表单形式
    let app = new Vue({
      el: '#app',
      created() {
        axios.defaults.baseURL = 'http://127.0.0.1:8080/'
        // 因为默认 Spring Security 是通过 Cookie 和 Session 来验证身份的,所以需要配置携带 Cookie
        axios.interceptors.request.use(config => {
          config.withCredentials = true;
          return config;
        });
      },
      data: {
        password: 'admin',
        username: 'admin'
      },
      methods: {
        sub() {
          axios.post('/doLogin', qs.stringify({
            username: this.username,
            password: this.password
          }), {
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded'
            }
          })
            .then((result) => {
              console.log(result.data);
            }).catch((err) => {
              console.log(err);
            });
        },
        sayHello() {
          axios.get('/hello')
            .then((result) => {
              console.log(result.data);
            }).catch((err) => {
              console.log(err);
            });
        },
        logout() {
            axios.post('/doLogout')
                .then((result) => {
                    console.log(result.data);
                }).catch((err) => {
                console.log(err);
            });
        }
      },
    })
  </script>
</body>
  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是Spring Security自带的登录的介绍和示例: Spring Security是一个基于Spring框架的安全框架,它提供了一套完整的安全解决方案,包括认证、授权、攻击防护等功能。Spring Security自带的登录功能可以让用户在登录后记住用户信息,下次访问时自动登录。 要使用Spring Security自带的登录功能,需要进行如下配置: 1.在pom.xml文件中添加Spring Security依赖: ```xml <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.5.0</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.5.0</version> </dependency> ``` 2.在Spring Security的配置类中添加如下代码: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").hasAnyRole("ADMIN", "USER") .anyRequest().authenticated() .and() .formLogin() .and() .rememberMe() .and() .logout() .logoutSuccessUrl("/login") .permitAll() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` 3.在Spring Security的配置类中添加一个UserDetailsService的实现类,用于从数据库中获取用户信息: ```java @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : user.getRoles()) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); } } ``` 4.在登录页面中添加如下代码: ```html <form action="/login" method="post"> <div> <label for="username">用户名:</label> <input type="text" id="username" name="username" /> </div> <div> <label for="password">密码:</label> <input type="password" id="password" name="password" /> </div> <div> <input type="checkbox" id="remember-me" name="remember-me" /> <label for="remember-me">记住我</label> </div> <div> <button type="submit">登录</button> </div> </form> ``` 5.在Spring Security的配置类中添加一个/login接口,用于处理登录请求: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // ... @Override protected void configure(HttpSecurity http) throws Exception { // ... http.formLogin() .loginPage("/login") .loginProcessingUrl("/login") .defaultSuccessUrl("/") .failureUrl("/login?error=true") .permitAll(); // ... } } ``` 以上就是Spring Security自带的登录功能的介绍和示例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值