22-07-12 西安 尚好房项目(03)Spring Security

Spring Security-用户认证

1、认证和授权

提出问题:在生产环境下我们如果不登录后台系统就可以完成这些功能操作吗?
一定需要登录。

提出问题:是不是所有的用户,只要登录成功就都可以操作所有功能呢?
当然不是,不同的用户拥有着不同的权限,用户只能执行拥有权限的操作

认证:登录的过程就是认证

系统提供的用于识别用户身份的功能,通常提供用户名和密码进行登录其实就是在进行认证,认证的目的是让系统知道你是谁

授权:户认证成功后,需要为用户授权

其实就是指定当前用户可以操作哪些功能

对后台系统进行权限控制,其本质就是对用户进行认证和授权,使用Spring Security可以帮助我们来简化认证和授权的过程。


2、依赖引入

访问后台管理系统会显示登录页面让管理员先登录才能访问

pom文件引入依赖

<!-- spring security安全框架 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.0.5.RELEASE</version>
</dependency>

认证和授权 是发生在处理器处理请求之前,所以web.xml中引入过滤器“SpringSecurity Filter

<!-- SpringSecurity Filter -->
<!-- DelegatingFilterProxy用于整合第三方框架(代理过滤器,非真正的过滤器,真正的过滤器需要在spring的配置文件) -->
<filter>
  <filter-name>springSecurityFilterChain</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
  <filter-name>springSecurityFilterChain</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

3、配置Spring Security

有2种配置的方式

  1. xml文件进行配置
  2. Java配置类进行配置(更简洁,更符合springboot的要求)

两种方式配置效果一致,我们使用第二种java类配置

web-admin项目中创建com.atguigu.config.WebSecurityConfig

配置类WebSecurityConfig的要求:
1.加上@Configuration,标识当前类为一个配置类
2.加上@EnableWebSecurity,开启SpringSecurity的功能
注意:这个配置类,一定要被Springmvc的包扫描扫到。

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}

启动项目,访问:http://localhost:8000/

url自动跳转到了一个默认的登录页面(框架自带的)

我们集成了SpringSecurity,效果就是首页访问不了,它会给你弹出登录页面,让你先登录


4、内存分配登录

这么做[内存分配]目的:让SpringSecurity知道它要校验的账号和密码是啥子

内存分配用户名密码
1、  重写configure(AuthenticationManagerBuilder auth)方法。

AuthenticationManagerBuilder直译:“身份验证管理器构建器“
2、  目前做的登录校验是不连接数据库的,
        直接告诉SpringSecurity一个固定的账号和密码:例如lucy,123456

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在SpringSecurity中的内存中设置账号为"lucy",密码为BCryptPasswordEncoder加密器加密后的"123456"
        auth.inMemoryAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("lucy")
                .password(new BCryptPasswordEncoder().encode("123456"))
                .roles("");
    }

为什么这个加密器BCryptPasswordEncoder会出现两次???

  1. 第一次:相当于注册时使用
  2. 第二次:相当于登录时校验

BCryptPasswordEncoder使用哈希算法+随机盐来对字符串加密。因为哈希是一种不可逆算法,所以密码认证时需要使用相同的算法+盐值来对待校验的明文进行加密,然后比较这两个密文来进行验证

加密器用new也太拉跨,spring不让你new,

使用@Bean放在ioc容器,改造后WebSecurityConfig如下:

@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在SpringSecurity中的内存中设置账号为"lucy",密码为BCryptPasswordEncoder加密器加密后的"123456"
        auth.inMemoryAuthentication()
                .withUser("lucy")
                .password(passwordEncoder.encode("123456"))
                .roles("");
    }

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

此时第一个new加密器的地方 就可以省略。原因如下:

.passwordEncoder(new BCryptPasswordEncoder())  这句话的意思,表示告诉SpringSecurity,在校验密码的时候,请使用什么加密器对密码进行加密后再校验。
但是SpringSecurity会自动到IOC容器中找加密器,是一个自动注入使用,所以可省略

接着,登录成功,但是SpringSecurity默认不允许页面嵌套,所以右边页面没法显示【使用的是Hplush框架】

解决页面嵌套无法显示的办法:

WebSecurityConfig中重写configure(HttpSecurity http)

@Override
protected void configure(HttpSecurity http) throws Exception {
    //必须调用父类的方法,否则就不需要认证即可访问
    super.configure(http);
    //允许页面嵌套
    http.headers().frameOptions().disable();
}

 再次访问,重新登录后,效果就有了

接着解决摆在我们面前的这俩件事,认证的功能就算做好了

1.优化登录页面,使更好看(和当前系统的主要题色调匹配)
2.连接数据库,真正进行账号和密码的校验


5、自定义登录页面

1、创建登录页面login.html、在spring-mvc.xml中配置视图控制器view-controller

<mvc:view-controller path="/login" view-name="frame/login"/>

2、在WebSecurityConfig配置类中重写configure(HttpSecurity http)

注意:登录页面和静态资源是不需要登录也能访问的

跨域就是:从一个域名发送请求访问另一个域名中的内容

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //允许页面嵌套
        http.headers().frameOptions().disable();
        //SpringSecurity的相关配置
        http
            .authorizeRequests()
            .antMatchers("/static/**","/login").permitAll()  //允许匿名用户访问的路径
            .anyRequest().authenticated()    // 其它页面全部需要验证
            .and()
            .formLogin()
            .loginPage("/login")    //指定登录时使用我们自定义的登录页面
            .defaultSuccessUrl("/") //登录认证成功后默认转跳的路径
            .and()
            .logout()
            .logoutUrl("/logout")   //表示退出登录请求的路径,这个请求是由SpringSecurity处理的
            .logoutSuccessUrl("/login");//表示退出登录之后跳转到哪个页面

        //关闭跨域请求伪造
        http.csrf().disable();
    }

再次访问,就是我们自定义的登录页面了,顺眼多了


6、UserDetailsService

Spring Security支持通过实现UserDetailsService接口的方式来提供用户认证授权信息

账号密码校验的时候会连接acl_admin表进行匹配

1、注释掉内存分配用户名和密码

2、创建UserDetailsServiceImpl类实现UserDetailsService接口   

      重写loadUserByUsername(String username),在该方法中进行登录校验

@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    @Reference
    private AdminService adminService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //username表示用户登录时候输入的账号
        //1. 我们要校验用户输入的账号是否正确:根据username到acl_admin表中查询
        Admin admin =adminService.getByUsername(username);
        if(null == admin) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //如果用户名存在,则将当前用户的密码交给SpringSecurity,让SpringSecurity去校验密码
        //创建一个User对象,并返回:参数1表示用户名、参数2表示密码、参数3表示当前用户拥有的权限列表(目前先指定为空)
        return new User(username,admin.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList(""));
    }
}

填坑:添加用户时对密码进行加密

admin的密码必须得使用加密器进行加密之后添加

SpringSecurity采用的加密器是BCryptPasswordEncoder,所以用户输入123456,SpringSecurity会将123456使用BCryptPasswordEncoder加密成密文之后再进行校验密码是否正确。

@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/save")
public String save(Admin admin, Model model){
    //设置密码
    admin.setPassword(passwordEncoder.encode(admin.getPassword()));
    adminService.insert(admin);
    return successPage(model,"新增用户成功");
}

填坑:左侧动态菜单

动态获取当前登录的用户的动态菜单

之前我们获取左侧动态菜单的时候,是写死用户为admin

现在可以用Spring Security获取登录的用户,从而拿到当前用户的菜单

 //获取当前登录的用户
 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 User user = (User) authentication.getPrincipal();

完整代码修改如下:

    @GetMapping("/")
    public String index(Model model){
        //获取当前登录的用户
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        Admin admin = adminService.getByUsername(user.getUsername());
        //2.根据用户id查用户的角色列表
        List<Role> roleList = roleService.findRoleListByAdminId(admin.getId());
        model.addAttribute("roleList",roleList);
        //查询用户的权限列表
        List<Permission> permissionList = permissionService.findMenuPermissionByAdminId(admin.getId());
        model.addAttribute("admin",admin);
        model.addAttribute("permissionList",permissionList);
        return PAGE_INDEX;
    }

测试动态菜单功能:

admin用户登录测试动态菜单功能:

aolafu用户登录测试动态菜单功能


Spring Security-权限校验

1、权限校验-用户授权

授权:当用户登录的时候,查询一个用户拥有哪些权限的code,然后告诉SpringSecurity

没有code表示这个权限并不需要SpringSecurity去校验,什么权限没有code?

一级菜单、二级菜单。因为一级菜单和二级菜单是否显示,是由动态菜单决定的,
根本不需要SpringSecurity校验。

查询用户拥有的权限code,

用户拥有的每一个权限,对应成一个字符串来交给SpringSecurity

代码逻辑不是很难,看点在于这里用到了stream流过滤、映射处理,用stream流过滤code为null的,再映射成Code的集合。

    @Override
    public List<String> findPermissionCodeListByAdminId(Long adminId) {
        //1. 判断adminId是否为1L(超级管理员,应该拥有一切权限)
        List<Permission> permissionList;
        if (adminId.equals(1L)) {
            //超级管理员
            permissionList = permissionMapper.findAll();
        } else {
            //不是超级管理员,根据用户id查询到用户的所有权限
            permissionList = permissionMapper.findPermissionListByAdminId(adminId);
        }

        //2. 将permissionList转换成permissionCodeList
        List<String> permissionCodeList = null;
        if (!CollectionUtils.isEmpty(permissionList)) {
            permissionCodeList = permissionList.stream()
                    .filter(permission -> !StringUtils.isEmpty(permission.getCode())) //过滤掉为空的code
                    .map(permission -> permission.getCode()) //将每一个permission转换成一个permissionCode
                    .collect(Collectors.toList());
        }
        return permissionCodeList;
    }

GrantedAuthority是接口。
也就是说我们要将每个code映射成一个SimpleGrantedAuthority对象。

看点:把一个Code集合,映射成为一个GrantedAuthority集合交给SpringSecurity管理。使用Stream流进行映射的,还用到了构造器引用

注意:

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    @Reference
    private AdminService adminService;
    @Reference
    private PermissionService permissionService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //username表示用户登录时候输入的账号
        //1. 我们要校验用户输入的账号是否正确:根据username到acl_admin表中查询
        Admin admin = adminService.getByUsername(username);
        if (admin == null) {
            //2. 用户名不正确
            throw new UsernameNotFoundException("username not found");
        }
        //2. 如果用户名正确,则将当前用户的密码交给SpringSecurity,让SpringSecurity去校验密码
        //创建一个User对象,并返回:参数1表示用户名、参数2表示密码、参数3表示当前用户拥有的权限列表(目前先指定为空)
        //2.1 查询出当前用户拥有的所有权限,然后告诉SpringSecurity
        List<String> permissionCodeList = permissionService.findPermissionCodeListByAdminId(admin.getId());

        //2.2 将permissionCodeList转成grantedAuthorityList
        List<GrantedAuthority> grantedAuthorityList = null;

        if (!CollectionUtils.isEmpty(permissionCodeList)) {
            grantedAuthorityList = permissionCodeList.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }else {
            grantedAuthorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("");
        }
        return new User(username, admin.getPassword(), grantedAuthorityList);
    }
}

SimpleGrantedAuthority源码如下:有助于理解构造器引用

public final class SimpleGrantedAuthority implements GrantedAuthority {
    private static final long serialVersionUID = 500L;
    private final String role;

    public SimpleGrantedAuthority(String role) {
        Assert.hasText(role, "A granted authority textual representation is required");
        this.role = role;
    }
}

我们先让aolafu用户拥有角色修改的权限,测试

测试aolafu用户是否可以修改角色信息

设置aolafu用户只有查看角色的权限,

重新测试,观察操作效果,

提出问题:为什么aolafu明明没有修改的权限,它还能进行修改?

因为修改的时候没有进行权限的校验,我们现在只做了授权,而没做权限校验!!!!


2、权限校验—权限校验

1、修改WebSecurityConfig配置类,开启Controller方法权限控制,添加下述注解

@EnableGlobalMethodSecurity(prePostEnabled = true)

2、给Controller方法添加权限注解  ,如

给各个Controller的控制器方法指定对应的操作权限,以角色管理的修改为例

@PreAuthorize("hasAnyAuthority('role.edit')")

    @PreAuthorize("hasAnyAuthority('role.edit')")
    @GetMapping("/edit/{id}")
    public String edit(Model model, @PathVariable Long id) {
        Role role = roleService.getById(id);
        model.addAttribute("role", role);
        return PAGE_EDIT;
    }

此时,再次使用aolafu用户登录(他的权限只有查看角色的功能)

测试:

403 权限拒绝,但是这个页面用户看着不太明白,为了优化用户体验,我们可以自定义访问拒绝处理器


3、自定义访问拒绝处理器

创建页面auth.html,使在权限拒绝的时候不再是403页面,而是我们自定义的这个页面

1、在spring-mvc.xml中配置view-controller

告诉SpringSecurity,当权限不够的时候就显示这个页面

<!--权限页面-->
<mvc:view-controller path="/auth" view-name="frame/auth"/>

2.创建自定义访问拒绝处理器AtguiguAccessDeniedHandler

public class AtguiguAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        //当访问拒绝的时候,我们怎么处理?
        //重定向访问"/auth"
        httpServletResponse.sendRedirect("/auth");
    }
}

3.配置自定义访问拒绝处理器(WebSecurityConfig中)
重写的方法configure(HttpSecurity http) 中加入这么一行配置

//指定自定义的访问拒绝处理器
http.exceptionHandling().accessDeniedHandler(new AtguiguAccessDeniedHandler());

这个方法configure(HttpSecurity http) 中现在有配置允许页面嵌套,自定义登录页面,自定义权限拒绝时使用的权限拒绝处理器,以下是完整版

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //允许页面嵌套
        http.headers().frameOptions().disable();
        //SpringSecurity的相关配置
        http
                .authorizeRequests()
                .antMatchers("/static/**","/login").permitAll()  //允许匿名用户访问的路径
                .anyRequest().authenticated()    // 其它页面全部需要验证
                .and()
                .formLogin()
                .loginPage("/login")    //用户未登录时,访问任何需要权限的资源都转跳到该路径,即登录页面,此时登陆成功后会继续跳转到第一次访问的资源页面(相当于被过滤了一下)
                .defaultSuccessUrl("/") //登录认证成功后默认转跳的路径
                .and()
                .logout()
                .logoutUrl("/logout")   //退出登陆的路径,指定spring security拦截的注销url,退出功能是security提供的
                .logoutSuccessUrl("/login");//用户退出后要被重定向的url

        //关闭跨域请求伪造
        http.csrf().disable();

        //指定自定义的访问拒绝处理器
        http.exceptionHandling().accessDeniedHandler(new AtguiguAccessDeniedHandler());
    }

优化后,再次测试,达到了我们想要的效果


4、页面功能按钮权限控制

再次优化,aolafu只有查看角色的权限,没有其它权限

需求:如果没有该权限,那么相关的按钮就别显示出来了

解决办法,让Thymeleaf与SpringSecurity整合起来。

父工程shf-parent已经管理了这个整合依赖 thymeleaf-extras-springsecurity5

1、直接在web-admin中引入

<!--用于springsecurity5标签-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

2、在Thymeleaf的模板引擎配置spring security 标签支持

<!--配置模板引擎-->
<bean id="templateEngine" class="org.thymeleaf.spring5.SpringTemplateEngine">
    <!--引用视图解析器-->
    <property name="templateResolver" ref="templateResolver"></property>
    <!-- 添加spring security 标签支持:sec -->
    <property name="additionalDialects">
        <set>
            <bean class="org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect" />
        </set>
    </property>
</bean>

3、页面按钮控制

  1. 在html文件里面声明使用spring-security标签
  2. 在相关按钮上使用标签  如修改按钮 sec:authorize="hasAuthority('role.edit')
<button type="button" class="btn btn-sm btn-primary create" sec:authorize="hasAuthority('role.create')">新增</button>

<a class="edit" th:attr="data-id=${item.id}" sec:authorize="hasAuthority('role.edit')">修改</a>
<a class="delete" th:attr="data-id=${item.id}" sec:authorize="hasAuthority('role.delete')">删除</a>
<a class="assgin" th:attr="data-id=${item.id}" sec:authorize="hasAuthority('role.assgin')">分配权限</a>

测试:效果如下,很满意

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值