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种配置的方式
- xml文件进行配置
- 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会出现两次???
- 第一次:相当于注册时使用
- 第二次:相当于登录时校验
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、页面按钮控制
- 在html文件里面声明使用spring-security标签
- 在相关按钮上使用标签 如修改按钮 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>
测试:效果如下,很满意