目录
一、Spring Security 入门使用
4、配置Spring Security,内存分配用户名密码,加密方式,授权
二、Spring Security 集成进阶 (自定义授权)
1. 登录、访问基础设置 (自定义页面)
2. 登录页面
3. 从数据库中查询用户信息
三、用户授权
1. 设置用户权限
2. 权限表中获取用户权限
3. controller 方法权限控制
4. 页面功能按钮权限控制
四、403(没有权限)统一处理
1、添加处理类,实现接口 AccessDeniedHandler
2、WebSecurityConfig 配置类进行授权
3、indexController添加映射、新增403页面
五、Spring Session 共享
1、添加依赖、版本管理
2、添加配置文件
3、spring-mvc.xml引入配置
4、添加session共享过滤器
前面我们已经完成了尚好房权限管理的部分相关功能,给用户分配角色,给角色分配权限,及左侧动态菜单,做好权限管理的数据准备,接下来我们要使用这些数据进行权限的相关控制。
Spring Security是 Spring提供的安全认证服务的框架。 使用Spring Security可以帮助我们来简化【认证】和【授权】的过程。
官网:Spring Security
中文官网:初识 Spring Security_w3cschool
Maven坐标:
org.springframework.security
spring-security-web
5.2.7.RELEASE
org.springframework.security
spring-security-config
5.2.7.RELEASE
1、添加版本管理
5.2.7.RELEASE
org.springframework.security
spring-security-web
${spring.security.version}
org.springframework.security
spring-security-config
${spring.security.version}
2、引入依赖
org.springframework.security
spring-security-web
org.springframework.security
spring-security-config
3、配置Spring Security Fillter
web.xml
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
配置Spring Security有两种方式:
1、xml文件配置
2、java类配置 (我们是用这种方式)
@Configuration //声明为配置类,相当于一个xml
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
/*认证:
1.基于内存的认证方式(了解)
2.基于数据库的认证方式(重要)*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于内存的认证方式,写死用户名称和密码,分配空的角色(没有权限)
auth.inMemoryAuthentication()
.withUser("lucy")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("");
}
//设置加密方式
//声明一个bean对象,等价于
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
//授权:
@Override
protected void configure(HttpSecurity http) throws Exception {
//默认授权: 不登陆系统,所有的资源都不允许访问
super.configure(http);
}
仅需三个步骤,我们就已经集成好了Spring Security,其他的事情就可以交给Spring Security为我们处理。启动项目,访问:http://localhost:8000/
前面我们已经完成了Spring Security的入门级配置,通过Spring Security的使用,Spring Security将我们项目中的所有资源都保护了起来,要访问这些资源必须要完成认证才能够访问。
但是这个案例中的使用方法离我们真实生产环境还差很远,还存在如下一些问题:
1、项目中我们将所有的资源(所有请求URL)都保护起来,实际环境下往往有一些资源不需要认证也可以访问,也就是可以匿名访问。
2、登录页面是由框架生成的,而我们的项目往往会使用自己的登录页面。
3、直接将用户名和密码配置在了java程序中,而真实生产环境下的用户名和密码往往保存在数据库中。
//授权:
@Override
protected void configure(HttpSecurity http) throws Exception {
//默认授权: 不登陆系统,所有的资源都不允许访问
//super.configure(http);
//自定义授权控制
//1.设置同源资源允许访问 同源(资源父路径一致的,协议,ip,port)的资源允许访问
http.headers().frameOptions().sameOrigin();
//2.授权静态资源不登录允许访问
http.authorizeRequests()
.antMatchers("/statis/**","/login").permitAll()
.anyRequest().authenticated();
//3.授权自定义的登录页面
//loginPage("/login")表示,通过这个映射跳转到自己的登录页,登陆成功后去向哪里
http.formLogin().loginPage("/login").defaultSuccessUrl("/");
//4.授权注销路径
//logoutUrl("/logout") 通过这个请求路径注销系统,销毁session,注销后去到哪
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
//5.关闭跨站请求伪造功能
//开启会自动生成
http.csrf().disable();
}
房
欢迎使用 尚好房平台管理系统
1. 启动基于数据库的认证方式,注入userDetailsService
@Autowired
UserDetailsService userDetailsService;
/*认证:
1.基于内存的认证方式(了解)
2.基于数据库的认证方式(重要)*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//1. 基于内存的认证方式,写死用户名称和密码,分配空的角色(没有权限)
/*auth.inMemoryAuthentication()
.withUser("lucy")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("");*/
//2.基于数据库的认证方式(重点)
auth.userDetailsService(userDetailsService);
}
2. 编写userDetailsService实现类
//加载用户信息权限集合
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Reference
AdminService adminService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.通过表单username获取admin对象,框架底层通过编码器自动解析密码进行匹配
Admin admin = adminService.getByUsername(username);
if(admin == null){
//return null;
throw new UsernameNotFoundException("用户名不存在");
}
// org.springframework.security.core.userdetails.User implements UserDetails
// User 是 UserDetails的实现类 权限集合暂时new为空
return new User(admin.getUsername(),admin.getPassword(), new ArrayList());
}
}
3.更改前往主页方法,动态获取用户
之前获取左侧菜单我们是写死了的,目前可以动态获取当前用户了
//框架首页
@RequestMapping("/")
public String index(Map map){
//Long adminId = 1L; //假设用户id=1
//Admin admin = adminService.getById(adminId);
//代码补充 TODO
//通过 SecurityContextHolder 从线程中获取认证对象
//(框架过滤器会将session域的用户对象存放到当前线程上(ThreadLocal)) 比session效率高
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();
Admin admin = adminService.getByUsername(user.getUsername());
//左侧菜单树,子节点通过双层for循环迭代生成的,当前集合值只存放父节点
List permissionList = permissionService.findMenuPermissionByAdminId(admin.getId());
map.put("admin",admin);
map.put("permissionList",permissionList);
return PAGE_FRAM;
}
//加载用户信息权限集合
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Reference
AdminService adminService;
@Reference
PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.通过表单username获取admin对象,框架底层通过编码器自动解析密码进行匹配
Admin admin = adminService.getByUsername(username);
if(admin == null){
//return null;
throw new UsernameNotFoundException("用户名不存在");
}
//2.1 获取选项权限集合
List codeList = null;
if(admin.getId() == 1){ //超级管理员
codeList = permissionService.findAllCode();
}else {
codeList = permissionService.findCodeListByAdminId(admin.getId());
}
//2.2 构建权限对象集合
Set auths = new HashSet();
if(codeList!=null && codeList.size()>0){
for (String code : codeList) {
//将code(权限字段)构建成权限对象,SimpleGrantedAuthority是GrantedAuthority的实现类
auths.add(new SimpleGrantedAuthority(code));
}
}
// org.springframework.security.core.userdetails.User implements UserDetails
// User 是 UserDetails的实现类
return new User(admin.getUsername(),admin.getPassword(), auths);
}
}
service
@Override
public List findCodeListByAdminId(Long id) {
return permissionDao.findCodeListByAdminId(id);
}
@Override
public List findAllCode() {
return permissionDao.findAllCode();
}
dao.xml
加入注解 @EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration //声明为配置类,相当于一个xml
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
//开启基于方法级别的细粒度权限控制 即在controller方法上加权限注解即可,例如@PreAuthorize("hasAuthority('role.show')")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
controller方法添加权限注解
//存储权限
//设置权限控制注解,在访问方法前需要校验控制权限
@PreAuthorize("hasAuthority('role.assgin') or hasRole('admin')")
@RequestMapping("/assignPermission")
public String assignPermission(@RequestParam("roleId") Long roleId,
@RequestParam("permissionIds") Long[] permissionIds, //Spring将字符串自动转换为数组
HttpServletRequest request){
permissionService.assignPermission(roleId,permissionIds);
return this.successPage(null,request);
}
//分配权限列表
@PreAuthorize("hasAuthority('role.assgin')")
@RequestMapping("/assignShow/{roleId}")
public String assignPermission(@PathVariable("roleId") Long roleId,Map map){
//{ id:2, pId:0, name:"随意勾选 2", checked:true, open:true},
List
上面我们完成了controller层方法的权限,现在我们要控制页面按钮的权限,如:角色管理上面只有查看权限,那么页面新增、修改、删除、分配权限按都不显示。
怎么实现呢?其实Spring Security已经给我们封装好了标签库,我们直接使用即可。
1.添加依赖
parent 版本管理:
3.0.4.RELEASE
org.thymeleaf.extras
thymeleaf-extras-springsecurity5
${thymeleaf-springsecurity5.version}
web-admin 引入依赖
org.thymeleaf.extras
thymeleaf-extras-springsecurity5
2. 配置文件 spring security 标签支持
修改spring-mvc.xml,在模板引擎配置spring security 标签支持
3. 页面按钮控制
1、在html文件里面申明使用spring-security标签
2、按钮上使用标签 sec:authorize
修改
删除
分配权限
//403未授权的统一处理方式
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.sendRedirect("/auth");
}
}
//授权:
@Override
protected void configure(HttpSecurity http) throws Exception {
//默认授权: 不登陆系统,所有的资源都不允许访问
//super.configure(http);
//自定义授权控制
//1.设置同源资源允许访问 同源(资源父路径一致的,协议,ip,port)的资源允许访问
http.headers().frameOptions().sameOrigin();
//2.授权静态资源不登录允许访问
http.authorizeRequests()
.antMatchers("/static/**","/login").permitAll()//允许匿名用户访问的路径
.anyRequest().authenticated();//其他页面需要验证
//3.授权自定义的登录页面
//loginPage("/login")表示,通过这个映射跳转到自己的登录页,登陆成功后去向哪里
http.formLogin().loginPage("/login").defaultSuccessUrl("/");
//4.授权注销路径
//logoutUrl("/logout") 通过这个请求路径注销系统,销毁session,注销后去到哪
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
//5.关闭跨站请求伪造功能
//开启会自动生成
http.csrf().disable();
//6.授权自定义的403权限不足处理类
http.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
}
private static final String PAGE_AUTH = "frame/auth";
//403统一处理
@GetMapping("/auth")
public String auth() {
return PAGE_AUTH;
}
没有权限
Session共享原理
用户第一次访问应用时,应用会创建一个新的 Session,并且会将 Session 的 ID 作为 Cookie 缓存在浏览器,下一次访问时请求的头部中带着该 Cookie,应用通过获取的 Session ID 进行查找,如果该 Session 存在且有效,则继续该请求,如果 Cookie 无效或者 Session 无效,则会重新生成一个新的 Session 在普通的 JavaEE 应用中,Session 信息放在内存中,当容器(如 Tomcat)关闭后,内存中的 Session 被销毁;重启后如果当前用户再去访问对应的是一个新的 Session ,在多实例中无法共享,一个用户只能访问指定的实例才能使用相同的 Session; Session 共享实现的原理是将原来内存中的 Session 放在一个需要共享 Session 的实例都可以访问到的位置,如数据库,Redis 中等,从而实现多实例 Session 共享 实现共享后,只要浏览器的 Cookie 中的 Session ID 没有改变,多个实例中的任意一个被销毁不会影响用户访问。
Spring Session共享原理
当请求进来的时候,SessionRepositoryFilter 会先拦截到请求,将 request 和 response 对象转换成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 。后续当第一次调用 request 的getSession方法时,会调用到 SessionRepositoryRequestWrapper 的
getSession
方法。这个方法是被从写过的,逻辑是先从 request 的属性中查找,如果找不到;再查找一个key值是"SESSION"的 Cookie,通过这个 Cookie 拿到 SessionId 去 Redis 中查找,如果查不到,就直接创建一个RedisSession 对象,同步到 Redis 中。说的简单点就是:拦截请求,将之前在服务器内存中进行 Session 创建销毁的动作,改成在 Redis 中创建。
我们以web-admin为例,web-front实现方式一样。
1.3.5.RELEASE
org.springframework.session
spring-session-data-redis
${redis-session.version}
web引入依赖
org.springframework.session
spring-session-data-redis
spring/spring-redis.xml
web
springSessionRepositoryFilter
org.springframework.web.filter.DelegatingFilterProxy
springSessionRepositoryFilter
/*
encode
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceRequestEncoding
true
forceResponseEncoding
true
encode
/*
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/*
springMVC
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring/spring-mvc.xml
1
springMVC
/
5、测试
说明:如果session没有同步到redis,那么再次重启,session信息已经清空,就会再次跳转登录,当前没有跳转登录,说明我们的session信息保存到redis。