菜鸟浅解SpringSecurity基本原理

Spring Security是Spring提供的安全组件,主要用于在项目中对用户的身份进行识别和认证。

Spring安全框架就是Spring-Security,功能是管理当前项目的用户登录和登录后的权限管理,是Spring框架提供的权限管理和安全方案

和Spring-Security框架功能类似的框架还有Shiro

使用Spring Security之前,需要添加依赖,可以在创建SpringBoot项目时直接勾选,也可以在已经创建好的项目中添加:


    org.springframework.boot
    spring-boot-starter-security

添加依赖之后,启动服务时,我们会看到一串随机生成的密码,这个密码可以用于登录Spring-Security的系统

用户名:user 密码为启动服务时生成的字符串

一旦加载这个依赖,那么要访问当前项目的资源就必须先登录Spring-Security

如果我们想自定义用户名和密码

那么就需要在application.properties文件中指定

代码如下

spring.security.user.name=admin
spring.security.user.password=666666

有上述配置,启动服务时就不会再生成随机密码,使用配置的用户名和密码即可登录

实际开发中,密码不可能使用明文保存.明文密码可以根据一定的加密算法加密为一个密文密码提高安全性,现在流行的加密算法有md5,Bcrypt等

我们项目中使用SpringSecurity自带的Bcrypt加密,下面我们就来实现一下使用Bcrypt对明文密码进行加密和验证的操作

首先来注入加密对象到Spring容器,在项目中创建一个security包,包中创建SecurityConfig类

// @Configuration表示当前这个类也是Spring的配置类
// Spring扫描到这类时,就会把它当做配置类解析
@Configuration
public class SecurityConfig {
    //向spring容器中注入一个加密对象,用于对密码加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

测试:

 @Autowired
    PasswordEncoder passwordEncoder;
    //加密
    @Test
    public void encode(){
        String pwd=passwordEncoder.encode("666666");
        System.out.println(pwd);
        //$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq
    }
    //验证
    @Test
    public void decode(){
        boolean bool=passwordEncoder.matches("666666",
   "$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq");
        System.out.println(bool);
    }

一旦项目注入了PasswordEncoder类型的对象,SpringSecurity框架就自动会使用这个对象对当前的明文密码进行加密,将application.properties文件中的用户的密码要修改为加密之后的了

application.properties登录配置修改如下

spring.security.user.name=admin
spring.security.user.password=$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq

需要密码加密的时候还需要在配置类中添加PasswordEncoder对象的注入很麻烦

我们可以在密文密码添加特定的算法idSpringSecurity就会自动按加密算法加密了,更简单

具体做法如下application.properties修改

spring.security.user.name=admin
spring.security.user.password={bcrypt}$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq

这样之前在SecurityConfig类中注入的PasswordEncoder就可以注释或删除了

@Configuration
public class SecurityConfig {

    //向spring容器中注入一个加密对象,用于对密码加密
    /*@Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }*/
}

在项目有很多用户,都是保存在数据库中的,不可能依靠配置文件的配置进行登录,所以我们要学习怎么在java代码中进行登录的验证,这一系列验证操作要编写在一个java的配置类中

正好使用我们上面章节中创建的SecurityConfig即可!!!

代码实例:

@Configuration
//下面的注解表示开启Spring-Security的权限管理功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //配置登录验证(即用户名和密码的验证)
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.inMemoryAuthentication().withUser("tom")
                .password("{bcrypt}$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq")
                //上面的配置是规定了用户的用户名和密码,可以使用他们登录
                //下面的配置是规定了这个用户的特定权限
                //有了这个特定权限才可以访问特定的方法
                .authorities("/user/get");

    }
}

上面的代码中,我们使用tom和正确的密码是可以访问一般资源的,但是如果有特殊权限要求的资源是无法访问的,下面我们就将之前编写的UserController类中的方法改写一下

在设计请求路径时,可以在请求路径中使用{}框住某个名称,用于表示某个变量,后续,当客户端提交请求时,{}占位符对应的位置可以是任何数据,都会被匹配到!当请求路径中使用了{ }占位符,在处理请求的方法的参数列表中,在参数的声明之前添加@PathVariable注解即可获取到占位符的值!将核心参数放在URL中,这是一种RESTFUL风格的API。

@RestController
@RequestMapping("/test")
public class TestController {
 
    @Autowired
    private IUserService userService;
 
    // http://localhost:8080/test/user/1
    @GetMapping("/user/{id}")
    public User getUserById(@PathVariable("id") Integer id) {
        return userService.getById(id);
    }
 
}

如果需要限制以上URL的访问,例如某些用户可以访问,但其他某些用户不可以访问,可以自行设计一个“权限字符串”,例如"a""hello"等均可!一般推荐使用URL的风格来定义访问权限,例如使用"test:user:info""/user/user/info"

@GetMapping("/user/{id}")
@PreAuthorize("hasAuthority('test:user:info')")
public User getUserById(@PathVariable("id") Integer id) {
    return userService.getById(id);
}

注意:权限字符串的设计与URL的设计没有任何关联!

可以在处理请求的方法之前配置@PreAuthorize注解,用于声明“访问该请求路径时必须具备某种权限”,例如:

代码示例:

//实际访问路径为:localhost:8080/v1/users/get?id=1
    @GetMapping("/get")
    @PreAuthorize("hasAuthority('/user/get')")//设置访问这个方法的特殊权限
    public User get(Integer id){
        User u=userService.getById(id);
        return u;
    }
    @GetMapping("/list")
    @PreAuthorize("hasAuthority('/user/list')")
    public List list(){
        List list=userService.list();
        return list;
    }

关于以上注解配置:

注解名称@PreAuthorize表示“在处理请求之前验证权限”;

注解属性中的hasAuthority表示“需要具备某种权限”;

注解属性中的/user/list是自定义的权限字符串,只是一种标识。

 用户登录-UserDetailsService接口  使用UserDetailsService提供认证数据

Spring Security定义了UserDetailsService接口,在接口存在抽象方法

UserDetailsService是SpringSecurity提供的一个接口,这个接口定义的方法返回一个UserDetails类型的对象,这个对象中包含用户的各种信息,用户名\密码\权限等

编写一个类来实现这个接口UserDetailsServiceImpl代码如下:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

 该方法的作用是:给定用户名,需要返回用户详情(UserDetails类型的对象),Spring Security获取到该用户详情后,会自动完成用户身份的验证,包括验证成功之后的用户权限信息,都是由框架处理的,作为开发人员,只需要解决“根据用户名获取用户详情”的问题即可!

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String name)
            throws UsernameNotFoundException {
        UserDetails user=null;
        //判断用户是不是jerry
        if("jerry".equals(name)){
            //设置jerry用户的详情
            user= User.builder()
                    .username("jerry")
                    .password("{bcrypt}$2a$10$ytxmGeWcRZObqoDmlhnWxe6KqUjb9DTONmQVKkmwneHQtZw4LQtiq")
                    .authorities("/user/get")
                    .build();
        }
        return user;
    }
}

注意:以上类必须在组件扫描的包中,并添加@Component注解,则Spring框架会自动创建以上类的对象并管理,后续就可以直接装配这个类的对象了!

然后,回到SecurityConfig类,应用以上类的对象:

然后,还需要在SecurityConfig类的声明之前添加@EnableGlobalMethodSecurity(prePostEnabled = true)注解,以允许执行访问权限的检查!例如:

@Configuration
//下面的注解表示开启Spring-Security的权限管理功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired(required = false)
    UserDetailsServiceImpl userDetailsService;

    //配置登录验证(即用户名和密码的验证)
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.userDetailsService(userDetailsService);

    }

}

严格意义上来说,以上方法并不是“登录”方法,只是一个“获取用户详情”的方法,甚至都不知道登录成功与否,所以,在参数列表中也没有密码,后续,将由Spring Security获取以上方法返回的对象,并验证密码是否正确等。

Spring-Security 使用连接查询获得用户的权限

查询用户详细信息需要两个方法,一个是根据用户名查询用户信息,还要上面根据用户id查询权限,这样才能获取所有登录时需要的资料

在UserMapper接口 示例:

@Repository
public interface UserMapper extends BaseMapper {

    // 根据用户的id查询用户的所有权限
    @Select("SELECT p.id,p.name" +
            "FROM user u" +
            "LEFT JOIN user_role ur ON u.id=ur.user_id" +
            "LEFT JOIN role r ON r.id=ur.role_id" +
            "LEFT JOIN role_permission rp ON r.id=rp.role_id" +
            "LEFT JOIN permission p ON p.id=rp.permission_id" +
            "WHERE u.id=#{id}")
    List findUserPermissionById(Integer id);

    //按用户名查询用户
    @Select("select * from user where username=#{username}")
    User findUserByUsername(String username);
}

编写的程序的基本准则是由我们自己的Service层调用我们自己写的Mapper,这次调用比较特殊,不是控制器调用Service而是SpringSecurity来调用,我们首先在IUserService接口中声明一个方法

public interface IUserService extends IService {

    //根据用户名获得用户认证信息的业务逻辑层方法
    UserDetails getUserDetails(String username);

}

然后在实现类UserServiceImpl 中实现:

@Service
public class UserServiceImpl extends ServiceImpl implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails getUserDetails(String username) {
        //根据用户名查询出用户对象
        User user=userMapper.findUserByUsername(username);
        // 如果用户为空,表示用户名不存在,返回登录失败
        if(user==null){
            return null;
        }
        //根据用户id查询用户权限
        List ps=userMapper
                .findUserPermissionById(user.getId());
        //将查询到的所有权限名称填装到一个数组中
        String[] auths=new String[ps.size()];
        int i=0;
        for(Permission p: ps){
            auths[i++]=p.getName();
        }
        //构建UserDetails
        UserDetails u= org.springframework.security.core
                .userdetails.User.builder()
                .username(user.getUsername())
                .password(user.getPassword())
                .authorities(auths)
                //getLocked默认都是0,所以要判==1得到false表示不锁定
                .accountLocked(user.getLocked()==1)
                //getEnabled默认都是1,所以要判==0得到false表示可用
                .disabled(user.getEnabled()==0)
                .build();
        return u;//一定不要返回null!!!!
    }
}

最后重构UserDetailsServiceImpl类中的代码 示例:

@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private IUserService userService;
    @Override
    public UserDetails loadUserByUsername(String name)
            throws UsernameNotFoundException {
        UserDetails user=userService.getUserDetails(name);
        return user;
    }
}

用户登录-关于访问控制(相当于拦截器)

控制授权范围和自定义登录页面授权范围控制

一个网站实际上并不是所有页面资源都需要登录才能访问,所以现在SpringSecurity的限制策略是比较严格的,如果我们想自己来设置这些授权范围就需要下面的代码,SecurityConfig类中添加一个方法代码如下

SecurityConfig中重写protected void configure(HttpSecurity http)方法:

/设置SpringSecurity的授权范围和登录页面
  @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()   //关闭防跨域攻击
            .authorizeRequests()//开始设置授权范围
            .antMatchers(
                    "/index.html",
                    "/img/*",
                    "/js/*",
                    "/css/*",
                    "/bower_components/**",
                    "/login.html"
                    ).permitAll()     //上面路径允许直接访问
                .anyRequest().authenticated()//其他的资源需要登录
                .and().formLogin()//登录方式是表单
                .loginPage("/login.html")//指定登录的页面
                .loginProcessingUrl("/login")//当登录页面提交时,会提交给哪个路径
                .failureUrl("/login.html?error")//登录失败从新登录,显示错误提示
                .defaultSuccessUrl("/index.html")//登录成功跳转到index.html
                .and().logout() //配置登出
                .logoutUrl("/logout")//设置登出路径
                //登出成功跳转登录页面,并提示已登出
                .logoutSuccessUrl("/login.html?logout");
    }
// 准备白名单,是不需要登录就可以访问的路径
    
    // 授权设置,是相对固定的配置
    // csrf().disable() > 关闭跨域攻击
    // authorizeRequests() > 对请求进行授权
    // antMatchers() > 配置访问白名单
    // permitAll() > 对白名单中的路径进行授权
    // anyRequest() > 其它的请求
    // authenticated() > 仅经过授权的允许访问,也可以理解为“未被授权将不允许访问”
    // and.formLogin() > 未被授权的将通过登录表单进行验证登录并授权
  • 调用参数对象http的链式方法的配置是相对固定的,可以尝试理解,也可以直接套用;
  • 以上调用的antMatchers( )相当于使用SpringMVC拦截器时配置白名单,方法的参数中,应该将所有需要被直接放行(不登录即可访问的位置)的路径都添加进来,例如:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 准备白名单,是不需要登录就可以访问的路径
       // String[] antMatchers = {
        //    "/index.html"
        //};
      
        String[] antMatchers = {
    	    "/index.html",
    	    "/bower_components/**",
            "/css/**",
            "/img/**",
            "/js/**"
        };
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(antMatchers).permitAll()
                .anyRequest().authenticated()
                .and().formLogin();
    }

    用户登录-更换自定义登录页

首先,在项目中添加Thymeleaf的依赖:


    org.thymeleaf.extras
    thymeleaf-extras-springsecurity5


    org.springframework.boot
    spring-boot-starter-thymeleaf

自定义的登录页面,将是被设计为HTML模版页,当请求登录的网址时,转发到该HTML模版页,则在项目的src/main/resoueces下创建templates文件夹,这是SpringBoot项目默认使用的模版页面文件夹,不需要配置,在转发时默认就会在这个文件夹中查询HTML模版文件,当文件夹创建完成后,将static文件夹下的login.html文件拖拽到templates文件夹下。

接下来,自定义控制器,设计登录页面的请求路径,在处理该路径的请求时,直接转发到/templates/login.html文件,由于Thymeleaf在整合时已经将前缀配置为了/templates/,把后缀配置为了.html,所以在控制器返回的视图名就是login:

在控制层新建一个控制器类用于显示login.html页面,名为SystemController代码如下

@RestController
public class SystemController {

    @GetMapping("/login.html")
    public ModelAndView loginForm(){
        return new ModelAndView("login");
    }

}
@Controller
public class SystemController {
 
    @GetMapping("/login.html")
    public String login() {
        return "login";
    }
 
    // 适用于使用@RestController时
    // public ModelAndView login() {
    //    return new ModelAndView("login");
    // }
 
}

 

 

 

 


 

你可能感兴趣的:(spring,boot)