什么是安全框架?是为了解决安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常麻烦。使用安全框架,我们可以通过的配置方式实现对资源的访问控制。
Spring Security是一个高度自定义的安全框架。利用 Spring 1oC/Dl和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。
使用Spring Secruity 的原因有很多,但大部分都是发现了javaEE的 Servlet 规范或 EJB规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务品环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。
正如你可能知道的两个应用程序的两个主要区域是认证"和"授权”(或者访问控制)。
这两点也是 Spring Security 重要核心功能。
“认证”,是建立一个他声明的主体的过程(一个“主体”—般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。
“授权"指确定—个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。
UserDetailsService
UserDetails
User
可以看到这个user类,这个user类,可不是我们平时定义的user类。这是SpringSecurity中定义的user类。
它最主要的有两个构造函数。
它这个password是我们根据userdetailsService的loadUserByUsername(username) 去数据库中查出来的数据。
是不是很像我们平时开发的逻辑。
在我们使用SpringSecurity做开发的时候,我们其实只有去实现两个接口。
一个是UserDetailsService、另一个就是PasswordEncoder。
自定义username
前面的根据是调用 loadUserByUsername(username) 去数据库查询用户,而要实现登陆的工能,就需要我们来自定义实现,所以就要用到PasswordEncoder接口。用来对用户数的密码加密,同时在和数据库中加过密的密码,来进行匹配。只要匹配成功,就可以实现登陆逻辑。
其实呢我们倒入依赖的时候,它就要实现。在我们的demo中,程序的启动之后控制台会打印一句话。
他的意思就是,自动生成的密码,在demo中我们自定义了,login 方法。
当我们在浏览器中使用login 方法,来实现登录的时候我们会发现。莫名其妙的弹出来一个login界面。我还以为是我自己写的。一想我咋可能写出这么好看的登录界面。确定的是,这肯定不是我自己写的。
所以呢,这就是SpringSecurity中自带的登录页面,目的就是为了,对用户进行拦截和认证。只要登录的用户才会被授权,进行后续的操作。
它规定的username,就是user,密码就是控制台打印的。
那倒我们要拿着玩意实现我们的登录逻辑嘛,显然是不可能。所以我们要再次基础上自定义我们的登录逻辑。
首先,我们肯定要重写 UserDetailsService 的 loadUserByUsername(username)的方法,为什么呢?
显然不同的用户有不同的username,所赋予的权限也不同。肯定需要重写。
接下来,我们平时的登录业务,大体上都是根据username,去数据库查询,查询到了返回整个对象实体,然后在根据前端用户输入的密码,和数据库查询带过来的对象实体中的密码,进行匹配。
可是,我们也看到,它这个密码显然是加过密了,所以呢,我们也需要对前端传过来的密码,也要进行一个加密,在和数据库中的密码进行匹配。当然了,在真实的开发,数据库当然是不会存明文密码,你懂的。
自定义登录密码
所以呢,我也需要实现这个PasswordEncoder接口。对我们用户输入的密码,进行加密。然后在登录的业务中,进行匹配。
那接下来,我们就看看这个接口。
常用的也就是,加密和匹配。它这个加密是不可逆的。
那我们简单的把玩一下这个 BCryptPasswordEncoder
@SpringBootTest
public class PasswordEncoder_test {
@Test
public void test(){
BCryptPasswordEncoder bc = new BCryptPasswordEncoder();
//对原始的密码进行加密
String encode = bc.encode("123");
System.out.println(encode);
//原始密码和加过密的进行匹配
boolean matches = bc.matches("123", encode);
System.out.println(matches);
}
}
//测试结果
$2a$10$cl18e/WgokCZsHpvdJWHbO7QOcDxZUWMVn5JtSAdIObpZ08JUs8XW
true
但是在我们的平常使用中springSecurity要求我们在spring的容器中有一个实例,所以我们平时都会一个配置类。配合@Configuration注解。@Bean注解。
在这里,我们拓展一下。使用SpringBoot做开发的同学。对下面的这个配置类,不少见吧。可是你真的知道这两个注解的作用吗?
@Configuration注解,就像当于我们在用Spring开发写的xml配置文件的作用。而@Bean就相当于我们在xml中写的标签。@Bean注解作用在方法上,声明当前方法的返回值是一个Bean。
package com.uin.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author wanglufei
* @description: SpringSecurity配置类
* @date 2022/4/10/7:51 PM
*/
@Configuration
public class SpringSecurityConfig{
/**
* BCryptPasswordEncoder实例
*
* @return org.springframework.security.crypto.password.PasswordEncoder
* @author wanglufei
* @date 2022/4/11 8:32 AM
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义登录。
package com.uin.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
* @author wanglufei
* @description: 实现SpringSecurity中的UserDetailsService
* @date 2022/4/11/8:43 AM
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
/**
* 自定义username
*
* @param username
* @return org.springframework.security.core.userdetails.UserDetails
* @author wanglufei
* @date 2022/4/11 8:44 AM
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据username去查询数据库,不存在的就会抛出异常UsernameNotFoundException
if (!"admin".equals(username)) {
return (UserDetails) new UsernameNotFoundException("用户名不存在");
}
//2.把查询的密码(注册是已经加过密)进行解析,或者直接把密码放入构造方法
String password = passwordEncoder.encode("123");
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList(
"admin,normal"));
}
}
我们会发现控制台并没有,打印刚才自动生成的密码,
发现是可以登录的。
当然自定义登录逻辑,远远不止于此。
我们不可能,一直用它这个登录页面吧,所以我们需要自定义登录页面。在SpringSecurity中有一个这样的类,提供我们做一些自定义的配置。
记得把super.configure(http)。这个是默认的配置。
页面太丑,反正实现效果了。
但是我们会发现一个问题,我们不登陆也可以访问main.html。这不就bbq了。所以我们要拦截请求。
但是在SpringSecurity里,它就做授权,授于你权利,让你去那个页面,就很像我们的在SSM中配置的拦截器。所以我们要转换一下概念,入乡随俗嘛。
这个就起到了,拦截器的作用。
哇涩,重定向次数过多。想了一下,好像我们去访问login.html页面也被拦截了,有点想一个死递归,死循环。
spring security 入门教程 详细讲解 - Caesar_the_great - 博客园 (cnblogs.com)
spring security 实现匿名访问 - Caesar_the_great - 博客园 (cnblogs.com)
自定义注解 @AnonymousAccess,写在需要匿名访问的接口上:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}
/**
* 功能描述: 登录
* @param loginVO 用户登录时的账号和密码
* @Description TODO
* @return com.rman.iflash.model.R
* @Author Caesar
* @Date 10:57 2020/5/3
**/
@AnonymousAccess
@PostMapping("/login")
public R login(@RequestBody @Validated LoginVO loginVO){
log.info("用户登录信息{}", loginVO);
LoginVO authUserDTO = new LoginVO();
authUserDTO.setUsername(loginVO.getUsername());
authUserDTO.setPassword(loginVO.getPassword());
authUserDTO.setCaptchaId(loginVO.getCaptchaId());
authUserDTO.setCaptchaCode(loginVO.getCaptchaCode());
Object data = loginService.login(authUserDTO);
return R.success(data);
}
在security的配置类的 configure(HttpSecurity http)方法中配置匿名访问:
//查找匿名标记URL
Map handlerMethods =
applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
Set anonymousUrls = new HashSet<>();
for (Map.Entry infoEntry : handlerMethods.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
if (anonymousAccess != null) {
anonymousUrls.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
}
}
anonymousUrls.forEach(s -> log.warn("可以匿名访问的url:{}", s));
http.antMatchers(anonymousUrls.toArray(new String[0])).anonymous()