写在前面:仅用于记录自己的学习
SpringSecurity
SpringSecurity
SpringSecurity融合了Spring技术栈,提供javaEE应用的整体安全解决方案,提供全面的安全服务。
目前市面上受欢迎的两个安全框架:Apacho Shiro(市场逐渐减小), SpringSecurity
用户提交登录请求的时候,如果登录成功,则可以查询用户的权限和角色集和绑定给该用户存到会话中;
用户本次会话内再次提交其他请求时,后台可以根据用户访问的资源的要求权限和用户的会话中的权限进行比较,如果用户包含资源要求的权限,那么就可以访问啦。
众筹整合SpringSecurity
授权配置-初始简单配置
1、引入依赖(在Impl的pom文件下(根据依赖的传递性))
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<!--<!–页面标签的权限控制–>-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
</dependency>
2、在main项目的web.xml中配置SpringSecurity的filter链
<!-- 权限过滤Filter -->
<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、创建SpringSecurity的配置类进行SpringSecurity配置
//@EnableGlobalMethodSecurity(prePostEnabled=true)//通过细粒度权限控制controller方法的访问,可以通过注解在我们的controller方法上绑定权限
@EnableWebSecurity //启用SpringSecurity权限验证功能
@Configuration//当前类是一个配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
4、启动项目,若出现如下页面则成功。
(因为SpringSecurity启动会验证权限,现在未具体配置权限设置,则认为所有的请求都需要权限验证,于是跳转到登录页面让我们创建主题。此时我们还没指定我们的登录页面,所以跳转的是其自己默认的)
授权配置-首页登录页面静态资源
需要实现下面两个方法
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
将首页、登录页面和静态资源授权所有人都可。
.authorizeRequests()开始对所有的请求授权
.antMatchers(xxx)对传入参数的多个路径进行授权
.permitAll()授权所有人都可以访问
.hasAnyRole("admin","boss")授权拥有角色的用户才可以访问
.hasAnyAuthority("user:add","user:del")授权拥有权限的用户才可以访问
.anyRequest()除了上面配置的其他所有配置请求
.authenticated()需要被授权
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置浏览器访问的路径
.antMatchers("/index", "/static/**","/login.html").permitAll()
.anyRequest().authenticated();
}
}
则可以访问首页和登录页面,但是点击登录按钮访问里面的页面时,就会报403了,因为未授权。
授权配置-登录注销配置
SpringSecurity可以对基于RBAC权限模型开发的项目进行主题创建销毁的管理,可以接管登录(主题创建)和注销(主题销毁)请求的处理。
1、将AdminController中处理登录注销的方法注销
2、在SpringSecurity配置类的授权方法内,指定具体SpringSecurity如何接管登录、注销请求
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//配置浏览器访问的路径
.antMatchers("/index", "/static/**","/login.html").permitAll()
.anyRequest().authenticated();
//权限框架默认启用CSRF,以后所有的post方式提交的表单请求必须携带csrf参数,否则dpringsecurity认为无权访问
http.csrf().disable();//禁用csrf防跨站点攻击功能
//登录请求的接管
http.formLogin()
.loginPage("/login.html")//提交登录请求的页面
.loginProcessingUrl("/doLogin")//登录请求的地址
.usernameParameter("loginacct")//登录账号参数的key
.passwordParameter("userpswd")//登录面参数的key
.defaultSuccessUrl("/admin/main.html");//登录成功重定向访问的地址
//注销请求的接管
http.logout()
.logoutUrl("/logout")//注销请求的地址
.logoutSuccessUrl("/login.html");//注销成功后跳转的页面地址
}
//认证:主体创建的过程(除了用户名密码,还要有权限)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中写固定的主体信息
auth.inMemoryAuthentication()
.withUser("lijie").password("123456").authorities("user:query","user:del")
.and()
.withUser("zhangsan").password("123456").roles("ADMIN","ZHUZE");
}
此时要注意加:http.csrf().disable();
,这段代码的作用是:禁用csrf防跨站点攻击功能。权限框架默认启用CSRF,以后所有的post方式提交的表单请求必须携带csrf参数,否则dpringsecurity认为无权访问
如果浏览器访问地址和之前写的不一样,需要修改之前前端页面。
在这里还修改了,如下,修改注销的超链接事件,使用form表单提交,以post方式提交注销请求给springsecurity。
<li><a class="logoutA" href="javascript:void(0);"><i class="glyphicon glyphicon-off"></i> 退出系统</a></li>
<form method="post" id="logoutForm" action="${PATH}/logout"></form>
window.onload=function () {
// 退出登录按钮的单击事件
$(".logoutA").click(function () {
layer.confirm("你真的要退出系统吗?",{"title":"注销确认","icon":3},function () {
layer.closeAll();
//点击确定提交注销请求
//get方法
<%--window.location="${PATH}/admin/logout";--%>
//使用表单提交post方法
$("#logoutForm").submit();
})
});
}
之后点击登录后没有反应,出现302状态码。
原因是自己在写代码的时候,由于疏忽,在登录请求那里少加了“/”
之后登录,会出现如下错误:
配置密码处理器,然后将之前设置的固定的密码改为加密后的。
向spring容器中注入Javabean对象的方式
1、在spring配置文件中通过
2、在组件类名上使用@组件注解
3、在组件类中通过@Bean注解标注到有返回值的方法上,该方法的返回值会自动注入带容器中
@Bean
public PasswordEncoder getPasswordEncoder() {
return new MessageDigestPasswordEncoder("md5");
}
登录成功后,点击注销出现如下错误:
原因是form表单那里多写了个“/”
csrf防跨站点攻击
csrf以后用的不多,现在大多都是验证码、图片等
解决方法:
认证配置-主体创建
从数据库中查询主题信息(用户信息+权限角色信息)
主体创建的业务类UserDetailsServiceImpl的业务方法会根据登录的账号查询主体对象,主要从以下方面:
1、根据登录的账号查询用户的信息
2、根据查询到的用户信息的id查询该用户的角色和权限信息(自定义mapper方法实现)
2.1 根据adminid查询用户角色信息
2.2 根据adminid查询用户权限信息
3、将查询到的用户信息+权限角色信息封装为User主体对象, 角色名称+权限名称字符串集和组成了权限集和。
遍历权限和角色名称封装为GrantedAuthority实体类的对象然后存到权限集和中表示权限, role为了和permission区分,需要手动在字符串前拼接ROLE_前缀。
3.1将角色集和封装为权限对象
3.2将权限集和封装为权限对象
@Autowired
UserDetailsService userDetailsService;
//认证:主体创建的过程(除了用户名密码,还要有权限)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中写固定的主体信息
/*auth.inMemoryAuthentication()
.withUser("lijie").password("e10adc3949ba59abbe56e057f20f883e").authorities("user:query", "user:del")
.and()
.withUser("zhangsan").password("e10adc3949ba59abbe56e057f20f883e").roles("ADMIN", "ZHUZE");
*/
//userDetailsService()用来给springsecurity配置一个主体创建的业务类对象
auth.userDetailsService(userDetailsService);
}
//主体创建的业务类
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
TAdminMapper adminMapper;
//根据登录的账号查询主体对象的业务方法
//UserDetails 主体类的接口
//User 主体类接口的实现
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1、根据登录的账号查询用户的信息
TAdminExample exa = new TAdminExample();
exa.createCriteria().andLoginacctEqualTo(username);
List<TAdmin> admins = adminMapper.selectByExample(exa);
if(CollectionUtils.isEmpty(admins)||admins.size()>1){
return null;
}
TAdmin admin=admins.get(0);
Integer id = admin.getId();
//2、根据查询到的用户信息的id查询该用户的角色和权限信息
//3、将查询到的用户信息+权限角色信息封装为User主体对象
return new User("username", "password", AuthorityUtils.createAuthorityList("xx", "xxx"));
}
}
2.1 根据adminid查询用户角色信息
2.2 根据adminid查询用户权限信息
在TRoleMapper.java和TPermissionMapper.java中写上述两个方
法
TRoleMapper.java:
//根据管理员id查询拥有的角色名称集和
List<String> selectRoleNamesByAdminId(Integer id);
TPermissionMapper.java:
//根据管理员id查询他拥有的权限名称列表的方法
List<String> selectPermissionNamesByAdminId(Integer id);
在impl项目中对应的的.xml文件实现上述方法(写SQL语句)
TRoleMapper.xml:
<!-- //根据管理员id查询拥有的角色名称集和
List<String> selectRoleNamesByAdminId(Integer id);
resultType表示查询到的一条记录要封装的数据类型
-->
<select id="selectRoleNamesByAdminId" resultType="java.lang.String">
SELECT tr.`name`
FROM t_admin_role tar
JOIN t_role tr
ON tar.`roleid`=tr.`id`
WHERE tar.`adminid`=#{id};
</select>
TPermissionMapper.xml:
<!--//根据管理员id查询他拥有的权限名称列表的方法
List<String> selectPermissionNamesByAdminId(Integer id);
-->
<select id="selectPermissionNamesByAdminId" resultType="java.lang.String">
SELECT DISTINCT t3.name
FROM t_admin_role t1
JOIN t_role_permission t2
ON t1.`roleid`=t2.`roleid`
JOIN t_permission t3
ON t2.`permissionid`=t3.`id`
WHERE t1.`adminid`=#{id} and t3.name is not null;
</select>
优化后完整代码:
@Service
//主体创建的业务类
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
TAdminMapper adminMapper;
@Autowired
TRoleMapper roleMapper;
@Autowired
TPermissionMapper permissionMapper;
//根据登录的账号查询主体对象的业务方法
//UserDetails 主体类的接口
//User 主体类接口的实现
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1、根据登录的账号查询用户的信息
TAdminExample exa = new TAdminExample();
exa.createCriteria().andLoginacctEqualTo(username);
List<TAdmin> admins = adminMapper.selectByExample(exa);
if (CollectionUtils.isEmpty(admins) || admins.size() > 1) {
return null;
}
TAdmin admin = admins.get(0);
Integer id = admin.getId();
//2、根据查询到的用户信息的id查询该用户的角色和权限信息
//自定义mapper方法实现
//2.1 根据adminid查询用户角色信息
List<String> roleNames = roleMapper.selectRoleNamesByAdminId(id);
List<GrantedAuthority> authorities = null;
//如果没有角色则一定没有权限列表,没有必要查询并封装权限集和,改过后发现,如果没有权限则登录失败
if (!(CollectionUtils.isEmpty(roleNames))) {
//2.2 根据adminid查询用户权限信息
List<String> permissionNames = permissionMapper.selectPermissionNamesByAdminId(id);
//3、将查询到的用户信息+权限角色信息封装为User主体对象
//角色名称+权限名称字符串集和组成了权限集和
//hasAnyRole()方法会对传入的角色字符串自动插入ROLE_前缀,表示角色
//我们对查询的角色的name值需要手动拼接ROLE_前缀表示角色,权限不用处理
// 遍历权限和角色名称封装为GrantedAuthority实体类的对象然后存到权限集和中表示权限
// role为了和permission区分,需要手动在字符串前拼接ROLE_前缀
//admin.getUserpswd()数据库中加密的密码
//利用(模仿其方法):AuthorityUtils.createAuthorityList("xx", "xxx")
//权限集合
authorities = new ArrayList<GrantedAuthority>();
//3.1将角色集和封装为权限对象
for (String role : roleNames) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
//3.2将权限集和封装为权限对象
for (String permission : permissionNames) {
authorities.add(new SimpleGrantedAuthority(permission));
}
}
System.out.println("=========admin========"+admin);
System.out.println("=========authorities========"+authorities);
return new User(admin.getLoginacct(), admin.getUserpswd(), authorities);
}
}
细粒度权限控制
方法一:在springsecurity配置类的授权方法中通过antMatcher指定授权(基本不用哈):
.antMatchers("/admin/delete").hasAnyRole("GL - 组长")
方法二:在controller方法上用@PreAuthorize注解结合SpringEL表达式授权(在配置类上要先加上@EnableGlobalMethodSecurity(prePostEnabled=true)
)
// 8、处理删除管理员请求
// @PreAuthorize:授权,支持springEL表达式,可以指定当前方法需要的权限和角色
@PreAuthorize("hasAnyAuthority('user','user:delete')")
@RequestMapping("/delete")
public String delete(@RequestParam("id") Integer id, Integer pageNum) {
adminService.deleteAdmin(id);
return "redirect:/admin/index?pageNum=" + pageNum;
}
测试发现不能用,原因如下:
springsecurity配置类使用的是@Configuration注解标注,由spring容器管理
而@Controller由springmvc容器管理
spring容器为父容器,springmvc为子容器,子容器可以使用父容器的内容,父容器不可以使用子容器
解决方案:
方案一:(不推荐使用)将授权设置到业务层的方法上;
@PreAuthorize("hasAnyAuthority('user','user:delete')")
@Override
public void deleteAdmin(Integer id) {
adminMapper.deleteByPrimaryKey(id);
}
方案二:将所有的spring配置文件都交给springmvc管理,只创建SpringMVC的容器。
将main项目下的web.xml文件中spring容器创建的初始化监听器以及spring初始化时指定配置文件的参数注释掉,之后在初始化springMVC容器的时候,加上classpath:spring/spring-*.xml
之后就可以啦。
补:
主体拥有user,user:delete权限或者ROLE_BZR-班主任角色就可以访问方法
@PreAuthorize("hasAnyAuthority('user','user:delete') or hasAnyRole('BZR - 班主任')")
主体拥有user,user:delete权限并且ROLE_BZR-班主任角色就可以访问方法
@PreAuthorize("hasAnyAuthority('user','user:delete') and hasAnyRole('BZR - 班主任')")
页面元素权限控制+登录状态的回显
在页面也可以授权标签的权限和角色,需要引入springsecurity的标签库
在manager_loginbar.jsp中,首先引入标签库:
<%@taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
然后获取主体的用户名(注意此处不是username):
<sec:authentication property="name"></sec:authentication>
最后:(此处注意hasAnyAuthority()和hasRole()使用时参数的区别,一个要手动加ROLE)
<sec:authorize access="hasAnyRole('BZR - 班主任')">
<button type="button" class="btn btn-default btn-danger">
<span class="glyphicon glyphicon-question-sign"></span> BZR帮助
</button>
</sec:authorize>
<sec:authorize access="hasAnyAuthority('ROLE_PM - 项目经理')">
<button type="button" class="btn btn-default btn-danger">
<span class="glyphicon glyphicon-question-sign"></span> 项目经理帮助
</button>
</sec:authorize>
<sec:authorize access="hasRole('PG - 程序员')">
<button type="button" class="btn btn-default btn-danger">
<span class="glyphicon glyphicon-question-sign"></span> 程序员帮助
</button>
</sec:authorize>
在刚刚开始报了如下错误,原因是自己在获取主体名是只写了<sec:authentication property=“name”>后面的</sec:authentication>没有写。
授权配置-自定义访问拒绝处理
springsecurity启用后,用户在访问未授权的页面时,默认响应系统403页面,用户体验差,不美观,我们可以给他配置自定义的访问拒绝的处理页面。
1、准备异常页面
2、在springsecurity的配置类中的授权方法内配置自定义访问拒绝处理,此处要将同步请求和异步请求分开处理,因为异步请求无法解析整个页面。
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
//当用户访问到未授权页面时的处理方法
@Override
public void handle(HttpServletRequest req, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//判断请求是同步还是异步
if ("XMLHttpRequest".equals(req.getHeader("X-Requested-With"))) {
//异步请求
response.getWriter().write("403");
} else {
//设置错误消息(重定向是session,转发是request域)
req.setAttribute("msg", accessDeniedException.getMessage());
//转发到403页面给用户提示
req.getRequestDispatcher("/WEB-INF/pages/error/403.jsp").forward(req, response);
//如果是ajax提交的异步请求被拒绝,那么不应该转发到403页面给ajax,无法解析
}
}
});
此外注意,如果是异步请求,页面中的异步请求方法需要判断响应结果,并做出对应的操作。
BCryptPasswordEncoder
md5加密的密码可以被暴力破解,因为同一个字符串每次加密的结果都一样,可以通过常用密码的字典破解比较简单的密码。
为了保证密码安全,BCrypt对同一个字符串加密后的结果每次都不一样,可以保证密码比较安全,验证也只能通过BCrypt验证。
简单学习:
@Test
public void test1() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123456");
//$2a$10$uwnZE/aaHldR94i2v6vEGu7w2s/FUfQWdtK8YsiBr9sEbMZURmn0S
//$2a$10$yQSQoczhR70TDLU0ZEXAFeICl6/ATeiSIvS3V0fzXqV06Y.IRKwGi
//长度64位,同一字符串每次加密结果都一样
System.out.println(encode);
boolean matches = passwordEncoder.matches("123456", "$2a$10$yQSQoczhR70TDLU0ZEXAFeICl6/ATeiSIvS3V0fzXqV06Y.IRKwGi");
System.out.println(matches);
}
1、修改springsecurity配置类中的注入到容器中的passwordEncoder对象为BCryptPasswordEncoder;
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
2、新增管理员业务代码中使用BCryptPasswordEncoder处理密码。
AdminServiceImpl.saveAdmin()
springsecurity原理
debug方式查看。
可以对web.xml中配置的springsecurity的filter打断点。filter配置了/*,代表过滤所有的请求,每次请求的时候都会被filter处理,调用filter的生命周期方法doFilter()
springsecurity可以通过它提供的过滤器过滤所有的请求;
当请求访问项目的时候,所有的请求被springsecurity的过滤器中内置的13个过滤器链处理,检查请求是否是登录、注销、记住我…检查请求是否有权限访问目标资源 ,决定是否放行。
我们在项目的web.xml中配置了springsecurity的filter,就是springsecurity处理权限的入口。当项目启动的时候,filter对象就创建了,当有请求访问项目资源时,该filter就会过滤请求服务器调用filter的生命周期方法
doFilter方法内,springsecurity又提供了13个filter链,依次去检查请求,如果提交的请求和springsecurity配置类中的登录配置匹配,那么本次请求就会被filter链中的UsernamepasswordAuthenticationFilter处理,处理过程中先从请求报文中判断请求是不是post方式,如果是解析请求账号密码,使用登录账号交给UserDetailServiceImpl的loaduserbyusername方法加载主体对象(数据库的账号信息+权限集和),如果可以加载到主体证明账号存在,再去判断密码是否正确,再将登录提交的密码和查询到的主体密码交给BCryptPasswordEncoder的matches方法验证是否一样,如果验证通过,本次登录成功,springsecurity会将查询到的主体对象存到session域中表示本次会话时的登录状态和权限信息。