前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >项目之用户登录和访问权限的控制(5)

项目之用户登录和访问权限的控制(5)

作者头像
海拥
发布2021-08-23 15:37:47
8250
发布2021-08-23 15:37:47
举报
文章被收录于专栏:全栈技术

13. 用户登录-准备工作

在开发注册功能时,在SecurityConfig类中配置以如下代码:

代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
}

以上代码的作用是关闭跨域攻击,如果没有以上代码,则执行异步请求时就会出错!

一旦添加了以上代码,却没有添加更多详细配置之前,Spring Security的登录拦截将不生效!为了便于开发登录功能,先暂时将以上代码去除(删除,或添加为注释)。

另外,在SecurityConfig中还有:

代码语言:javascript
复制
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

以上代码的作用是创建密码加密器对象并交给Spring容器进行管理,以至于需要执行密码加密时,直接自动装配密码加密器即可!

目前,为了保证能够正确登录,需要将以上密码加密器去除,因为,开发完注册功能后,用户注册成功后的密码已经使用密文的形式存储在数据库中了,并且添加了{bcrypt}前缀用于声明加密时使用的算法,Spring Security会自动使用以上代码装配的PasswordEncoder执行1次加密,还会再因为{bcrypt}前缀再执行1次加密,就会导致登录验证失败!

【小结】密文使用${bcrypt}前缀,和让Spring容器管理BcryptPasswordEncoder这2个做法只能二选一!

一旦去除以上代码,就会导致Spring容器中没有PasswordEncoder对象了,但是,在UserServiceImpl中还需要使用到它,则应该将其调整为自行创建的模式,即:

代码语言:javascript
复制
// @Autowired // 需要去除自动装配注解
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

14. 用户登录-基于内存验证的模拟登录

先将application.properties中配置的Spring Security的用户名和密码去除!

然后,在SecurityConfig类(继承自WebSecurityConfigurerAdapter的配置类)中重写protected void configure(AuthenticationManagerBuilder auth)方法,并在这个方法中配置允许使用的用户名、密码及该账号的权限:

代码语言:javascript
复制
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .withUser("java")
            .password("{bcrypt}$2a$10$tsM03ULkiifEpSCWtQ5Mq.yrLZIPKVr5vHwU1FGjtT9B1vPlswa.C")
            .authorities("/test");
}

以上密文密码的原文是1234。

注意:配置以上代码时,必须调用authorities()以配置授权范围,如果没有配置,将会启动失败,由于当前尚未配置各请求所需要具备的权限,所以,关于以上范围,可以暂时使用任意字符串表示。

15. 用户登录-UserDetailsService接口

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

代码语言:javascript
复制
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

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

可以在cn.tedu.straw.portal.security包中创建UserDetailsServiceImpl类,实现以上接口,模拟实现获取用户数据:

代码语言:javascript
复制
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 假设正确的用户名是security
        // 假设用户名是正确值:security
        if ("security".equals(username)) {
            // 通过Spring-Security提供的User类来构建UserDetails对象
            UserDetails userDetails = User.builder()
                    .username("security")
                    .password("{bcrypt}$2a$10$tsM03ULkiifEpSCWtQ5Mq.yrLZIPKVr5vHwU1FGjtT9B1vPlswa.C")
                    .authorities("test")
                    .build();
            return userDetails;
        }
        return null;
    }

}

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

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

代码语言:javascript
复制
@Autowired
UserDetailsServiceImpl userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
}

注意:以上全局属性声明为UserDetailsServiceImpl类型,不可以声明为其接口类型,因为接口类型的对象不只1个。

16. 用户登录-查询数据库验证登录

先在IUserService接口中添加抽象方法:

代码语言:javascript
复制
UserDetails login(String username);

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

然后,在UserServiceImpl实现类中重写以上抽象方法:

代码语言:javascript
复制
@Override
public UserDetails login(String username) {
    // 根据参数username查询用户信息
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("username", username);
    User user = userMapper.selectOne(queryWrapper);
    // 判断查询结果是否为null,即:有没有这个用户
    // 注意:后续的验证和最终的界面是由Spring-Security显示的,此处不要抛出异常
    if (user == null) {
        return null;
    }
    // 组织“用户详情”对象
    // TODO 未完
    UserDetails userDetails = org.springframework.security.core.userdetails.User
            .builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities("test")
            .build();
    return userDetails;
}

完成后,在src/test/java下的cn.tedu.straw.portal.service.UserServiceTests编写并执行单元测试:

代码语言:javascript
复制
@Test
void login() {
    String username = "13988139111";
    UserDetails userDetails = userService.login(username);
    log.debug("login, user details={}", userDetails);
}

如果测试通过,就可以把以上获取得到的UserDetails对象应用到UserDetailsServiceImpl的返回值中:

代码语言:javascript
复制
@Component
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private IUserService userService;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userService.login(username);
    }

}

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

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

代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 准备白名单,是不需要登录就可以访问的路径
    String[] antMatchers = {
        "/index.html"
    };
    // 授权设置,是相对固定的配置
    // csrf().disable() > 关闭跨域攻击
    // authorizeRequests() > 对请求进行授权
    // antMatchers() > 配置访问白名单
    // permitAll() > 对白名单中的路径进行授权
    // anyRequest() > 其它的请求
    // authenticated() > 仅经过授权的允许访问,也可以理解为“未被授权将不允许访问”
    // and.formLogin() > 未被授权的将通过登录表单进行验证登录并授权
    http.csrf().disable()
            .authorizeRequests()
            .antMatchers(antMatchers).permitAll()
            .anyRequest().authenticated()
            .and().formLogin();
}

关于以上代码:

至今,主页index.html是不需要登录即可访问的,而其它页面暂时都是需要登录才允许访问的!

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

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

代码语言:javascript
复制
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

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

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

代码语言:javascript
复制
@Controller
public class SystemController {

    @GetMapping("/login.html")
    public String login() {
        return "login";
    }

    // 适用于使用@RestController时
    // public ModelAndView login() {
    //    return new ModelAndView("login");
    // }

}

然后,还需要将以上设计的请求路径添加到配置的白名单中。

完成后,重启项目,在浏览器通过http://localhost:8080/login.html即可看到自定义的登录页面。

目前,通过http://localhost:8080/login.html可以访问到自定义的登录页,并且,通过http://localhost:8080/login还能访问到Spring Security内置的登录页,也就是说,这2个登录页面是共存的!应该通过配置,使得Spring Security始终自动使用我们自定义的登录页!需要在SecurityConfig类的配置中补充添加:

代码语言:javascript
复制
package cn.tedu.straw.portal.security;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

//    @Bean
//    public PasswordEncoder passwordEncoder() {
//        return new BCryptPasswordEncoder();
//    }

//    @Override
//    protected void configure(HttpSecurity http) throws Exception {
//        http.csrf().disable();
//    }

    @Autowired
    UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 登录页面的URL
        String loginPageUrl = "/login.html";
        // 处理登录请求的URL
        String loginProcessingUrl = "/login";
        // 登录失败后的URL
        String loginFailureUrl = "/login.html?error";
        // 登录成功后的URL
        String loginSuccessUrl = "/index.html";
        // 退出登录的URL
        String logoutUrl = "/logout";
        // 退出登录成功后的URL
        String logoutSuccessUrl = "/login.html?logout";
        // 准备白名单,是不需要登录就可以访问的路径
        String[] antMatchers = {
                loginPageUrl,
                "/index.html",
                "/bower_components/**",
                "/css/**",
                "/img/**",
                "/js/**"
        };
        // 授权设置,是相对固定的配置
        // csrf().disable() > 关闭跨域攻击
        // authorizeRequests() > 对请求进行授权
        // antMatchers() > 配置访问白名单
        // permitAll() > 对白名单中的路径进行授权
        // anyRequest() > 其它的请求
        // authenticated() > 仅经过授权的允许访问,也可以理解为“未被授权将不允许访问”
        // and.formLogin() > 未被授权的将通过登录表单进行验证登录并授权
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(antMatchers).permitAll()
                .anyRequest().authenticated()
                .and().formLogin()
                .loginPage(loginPageUrl)
                .loginProcessingUrl(loginProcessingUrl)
                .failureUrl(loginFailureUrl)
                .defaultSuccessUrl(loginSuccessUrl)
                .and().logout()
                .logoutUrl(logoutUrl)
                .logoutSuccessUrl(logoutSuccessUrl);
    }

}

19. 关于访问权限控制

先准备一下测试使用的URL:

代码语言:javascript
复制
@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);
    }

}

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

完成后,可以通过http://localhost:8080/test/user/1进行访问。

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

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

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

代码语言:javascript
复制
@GetMapping("/user/{id}")
@PreAuthorize("hasAuthority('test:user:info')")
public User getUserById(@PathVariable("id") Integer id) {
    return userService.getById(id);
}

关于以上注解配置:

  • 注解名称@PreAuthorize表示“在处理请求之前验证权限”;
  • 注解属性中的hasAuthority表示“需要具备某种权限”;
  • 注解属性中的test:user:info是自定义的权限字符串,只是一种标识。

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

代码语言:javascript
复制
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 忽略类中的代码
    
}

如果再次访问http://localhost:8080/test/user/1,由于没有权限,会导致AccessDeniedException,并且,由于当前项目中使用了统一处理异常的机制,所有未知的异常也会被处理,可以看到表示错误信息的JSON数据。

可以尝试直接添加权限,使得用户可以访问以上URL,例如,在业务层实现类中,在处理“获取用户详情”时,为该用户详情封装匹配的权限字符串(与控制器要求的权限字符串保持一致即可):

代码语言:javascript
复制
// 权限字符串数组
String[] authorities = {
    "test:user:info"
};
// 组织“用户详情”对象
// TODO 未完
UserDetails userDetails = org.springframework.security.core.userdetails.User
        .builder()
        .username(user.getUsername())
        .password(user.getPassword())
        .authorities(authorities)
        .build();

根据用户的id,查出该用户所具有的权限(List<Permission>)。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/07/26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 13. 用户登录-准备工作
  • 14. 用户登录-基于内存验证的模拟登录
  • 15. 用户登录-UserDetailsService接口
  • 16. 用户登录-查询数据库验证登录
  • 17. 用户登录-关于访问控制(相当于拦截器)
  • 18. 用户登录-更换自定义登录页
  • 19. 关于访问权限控制
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档