前面我们已经完成了尚好房权限管理的部分相关功能,给用户分配角色,给角色分配权限,及左侧动态菜单,做好权限管理的数据准备,接下来我们要使用这些数据进行权限的相关控制。
现在我们需要思考2个问题:
*问题1*:在生产环境下我们如果不登录后台系统就可以完成这些功能操作吗?
答案显然是否定的,要操作这些功能必须首先登录到系统才可以。
*问题2*:是不是所有用户,只要登录成功就都可以操作所有功能呢?
答案是否定的,并不是所有的用户都可以操作这些功能。不同的用户可能拥有不同的权限,这就需要进行授权了。(用户登录之后,对每个用户进行授权,通过授权去访问系统中不同的功能–>授权)
*认证*:系统提供的用于识别用户身份的功能,通常提供用户名和密码进行登录其实就是在进行认证,认证的目的是让系统知道你是谁。
*授权*:用户认证成功后,需要为用户授权,其实就是指定当前用户可以操作哪些功能。
本章节就是要对后台系统进行权限控制,其本质就是对用户进行认证和授权。
Spring Security是 Spring提供的安全认证服务的框架。 使用Spring Security可以帮助我们来简化认证和授权的过程。
官网:https://spring.io/projects/spring-security/
中文官网:https://www.w3cschool.cn/springsecurity/
对应的maven坐标:
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-webartifactId>
<version>5.2.7.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-configartifactId>
<version>5.2.7.RELEASEversion>
dependency>
常用的权限框架除了Spring Security,还有Apache的shiro框架。
目标: 让用户访问管理后台中的资源的时候,需要输入用户名和密码进行登录
pom.xml
<spring.security.version>5.2.7.RELEASEspring.security.version>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-webartifactId>
<version>${spring.security.version}version>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-configartifactId>
<version>${spring.security.version}version>
dependency>
目前只是我们的后台管理系统需要授权与认证
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-configartifactId>
dependency>
dependencies>
web.xml
<filter>
<filter-name>springSecurityFilterChainfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
filter>
<filter-mapping>
<filter-name>springSecurityFilterChainfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
配置Spring Security
有两种方式:
1、xml文件配置
2、java类配置
两种方式配置效果一致,当前我们使用java类配置,更加简洁,我们在web-admin
项目中创建com.atguigu.config.WebSecurityConfig
类
package com.atguigu.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity //@EnableWebSecurity是开启SpringSecurity的默认行为
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
仅需三个步骤,我们就已经集成好了Spring Security
,其他的事情就可以交给Spring Security
为我们处理。
启动项目
访问:http://localhost:8000/
所有资源访问受限(包括静态资源)
url
自动跳转到了一个默认的登录页面(框架自带的),我们目前没有定义login
页面及login controller
方法。
但是当前没有账号啊!下面我们测试一个最简单的内存分配用户名密码。
操作类:WebSecurityConfig
重写configure(AuthenticationManagerBuilder auth)
方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("lucy")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("");
}
请求:http://localhost:8000/
报错:springsecurity There is no PasswordEncoder mapped for the id "null"
需要设置加密方式
/**
* 必须指定加密方式,上下加密方式要一致
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
登录成功,但是iframe
部分页面不显示
默认Spring Security
不允许iframe
嵌套显示,我们需要设置
@Override
protected void configure(HttpSecurity http) throws Exception {
//必须调用父类的方法,否则就不需要认证即可访问
super.configure(http);
//允许iframe嵌套显示
http.headers().frameOptions().disable();
}
到目前为止,我们通过内存分配用户名密码的方式,可以访问后台页面了。
前面我们已经完成了Spring Security
的入门级配置,通过Spring Security
的使用,Spring Security
将我们项目中的所有资源都保护了起来,要访问这些资源必须要完成认证才能够访问。
但是这个案例中的使用方法离我们真实生产环境还差很远,还存在如下一些问题:
1、项目中我们将所有的资源(所有请求URL)都保护起来,实际环境下往往有一些资源不需要认证也可以访问,也就是可以匿名访问。
2、登录页面是由框架生成的,而我们的项目往往会使用自己的登录页面。
3、直接将用户名和密码配置在了java程序中,而真实生产环境下的用户名和密码往往保存在数据库中。
现在我们需要对这些问题进行改进。
① 在web-admin
项目中创建templates/frame/login.html
页面
DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:include="common/head :: head">head>
<body class="gray-bg">
<div class="middle-box text-center loginscreen animated fadeInDown">
<div>
<div>
<h1 class="logo-name">房h1>
div>
<h3>欢迎使用 尚好房平台管理系统h3>
<form class="m-t" role="form" th:action="@{/login}" method="post">
<label style="color:red;" th:if="${param.error}" th:text="用户名或密码错误">label>
<div class="form-group">
<input type="text" name="username" value="admin" class="form-control" placeholder="用户名" required="">
div>
<div class="form-group">
<input type="password" name="password" value="123456" class="form-control" placeholder="密码" required="">
div>
<button type="submit" class="btn btn-primary block full-width m-b">登 录button>
<p class="text-muted text-center"> <a href="javascript:"><small>忘记密码了?small>a> | <a href="javascript:">注册一个新账号a>
p>
form>
div>
div>
body>
html>
② 在spring-mvc.xml
中配置访问登录页面的请求映射
<mvc:view-controller path="/login" view-name="frame/login"/>
在WebSecurityConfig配置类中重写如下方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
//允许iframe嵌套显示
http.headers().frameOptions().disable();
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();
}
/*@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("lucy")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("");
}*/
在service-acl
项目的com.atguigu.mapper.AdminMapper
接口中新增方法
Admin getByUsername(String username);
在service-acl
项目的resources/mappers/AdminMapper.xml
中新增
<select id="getByUsername" resultType="Admin">
<include refid="columns">include>
from acl_admin
where
username = #{username}
and is_deleted = 0
select>
在service-api
项目的com.atguigu.service.AdminService
接口中新增方法
Admin getByUsername(String username);
在service-acl
项目的com.atguigu.service.impl.AdminServiceImpl
实现类中新增方法
@Override
public Admin getByUsername(String username) {
return adminMapper.getByUsername(username);
}
Spring Security支持通过实现UserDetailsService接口的方式来提供用户认证授权信息
我们在web-admin
项目中创建com.atguigu.config.UserDetailsServiceImpl
实现类
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Reference
private AdminService adminService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查找用户
Admin admin =adminService.getByUsername(username);
if(null == admin) {
throw new UsernameNotFoundException("用户名不存在!");
}
//校验密码,操作权限目前用空的
return new User(username,admin.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
前面添加用户是我们没有对密码进行加密处理,现在改造。删除未加密的数据记录,重新创建用户信息
修改com.atguigu.controller.AdminController
类save
方法
@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
获取登录的用户
修改web-admin
项目中的com.atguigu.controller.IndexController
类的index()
方法
@GetMapping("/")
public String index(Model model){
//获取当前登录的用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
Admin admin = adminService.getByUsername(user.getUsername());
//查询用户的权限列表
List<Permission> permissionList = permissionService.findMenuPermissionByAdminId(admin.getId());
model.addAttribute("admin",admin);
model.addAttribute("permissionList",permissionList);
return PAGE_INDEX;
}
在service-acl
项目的com.atguigu.mapper.PermissionMapper
接口中新增
/**
* 查询用户的操作权限
* @param adminId
* @return
*/
List<String> findCodePermissionListByAdminId(Long adminId);
/**
* 查询所有操作权限
* @return
*/
List<String> findAllCodePermission();
在service-acl
项目中的resources/mappers/PermissionMapper.xml
中新增
<select id="findAllCodePermission" resultType="string">
select code from acl_permission where is_deleted=0
select>
<select id="findCodePermissionListByAdminId" resultType="string">
SELECT code
FROM acl_permission
WHERE id IN (
SELECT permission_id FROM acl_role_permission WHERE role_id IN (
SELECT role_id FROM acl_admin_role WHERE admin_id=#{adminId} AND is_deleted=0
) AND is_deleted=0
) AND TYPE=2 AND is_deleted=0
select>
在service-api
项目的com.atguigu.service.PermissionService
接口中新增
/**
* 查询用户的操作权限
* @param adminId
* @return
*/
List<String> findCodePermissionListByAdminId(Long adminId);
在service-acl
项目的com.atguigu.service.impl.PermissionServiceImpl
实现类中新增
@Override
public List<String> findCodePermissionListByAdminId(Long adminId) {
//判断是否是超级管理员
if (adminId == 1) {
//拥有所有权限
return permissionMapper.findAllCodePermission();
}
return permissionMapper.findCodePermissionListByAdminId(adminId);
}
修改web-admin
项目的com.atguigu.config.UserDetailsServiceImpl
类
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Reference
private AdminService adminService;
@Reference
protected PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Admin admin = adminService.getByUsername(username);
if(null == admin) {
throw new UsernameNotFoundException("用户名不存在!");
}
//获取用户权限列表
List<String> codePermissionList = permissionService.findCodePermissionListByAdminId(admin.getId());
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
for (String code : codePermissionList) {
if(StringUtils.isEmpty(code)) {
continue;
}
grantedAuthorityList.add(new SimpleGrantedAuthority(code));
}
return new User(username,admin.getPassword(), grantedAuthorityList);
}
}
**目标:**给各个Controller的方法指定对应的操作权限,以角色管理增删改查等为例
修改web-admin
项目中的com.atguigu.config.WebSecurityConfig
配置类,添加下述注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
修改web-admin
项目中的com.atguigu.controller.RoleController
类
@Controller
@RequestMapping("/role")
public class RoleController extends BaseController {
@Reference
private RoleService roleService;
@Reference
private PermissionService permissionService;
private final static String LIST_ACTION = "redirect:/role";
private static final String PAGE_ASSIGN_SHOW = "role/assignShow";
@PreAuthorize("hasAnyAuthority('role.show')")
@RequestMapping
public String index(@RequestParam Map conditions, Model model){
if(!conditions.containsKey("pageNum")) {
conditions.put("pageNum", 1);
}
if(!conditions.containsKey("pageSize")) {
conditions.put("pageSize", 10);
}
PageInfo<Role> pageInfo = roleService.findPage(conditions);
model.addAttribute("page", pageInfo);
model.addAttribute("conditions", conditions);
return "role/index";
}
@PreAuthorize("hasAnyAuthority('role.create')")
@PostMapping("/saveRole")
public String saveRole(Role role, Model model){
roleService.insert(role);
return successPage(model,"添加角色成功");
}
@PreAuthorize("hasAnyAuthority('role.show')")
@GetMapping("/findById/{id}")
public String findRoleById(@PathVariable("id") Long id,Model model){
Role role = roleService.getById(id);
model.addAttribute("role",role);
return "role/edit";
}
@PreAuthorize("hasAnyAuthority('role.edit')")
@PostMapping("/updateRole")
public String updateRole(Role role,Model model){
roleService.update(role);
return successPage(model,"更新角色成功");
}
@PreAuthorize("hasAnyAuthority('role.delete')")
@GetMapping("/delete/{id}")
public String deleteRoleById(@PathVariable("id") Long id){
roleService.delete(id);
return LIST_ACTION;
}
@PreAuthorize("hasAnyAuthority('role.assgin')")
@GetMapping("/assignShow/{roleId}")
public String assignShow(@PathVariable("roleId") Long roleId,Model model){
List<Map<String, Object>> zNodes = permissionService.findPermissionByRoleId(roleId);
model.addAttribute("zNodes", JSON.toJSONString(zNodes));
model.addAttribute("roleId",roleId);
return PAGE_ASSIGN_SHOW;
}
@PreAuthorize("hasAnyAuthority('role.assgin')")
@PostMapping("/assignPermission")
public String assignPermission(@RequestParam("roleId") Long roleId,
@RequestParam("permissionIds") List<Long> permissionIds,
Model model){
permissionService.saveRolePermission(roleId,permissionIds);
return successPage(model,"设置角色权限成功");
}
}
目前已admin登录,这些权限都有,点击角色管理相关功能都能正常访问。使用Admin设置某个用户只有查看角色的权限
然后admin退出登录,使用只有查看角色权限的用户登录,观察操作效果
不能访问了,提示403错误状态,表示没有访问权限。
上面这样提示很不友好,我们自定义提示页面
在web-admin
项目中创建templates/frame/auth.html
DOCTYPE html>
<html>
<head>
head>
<body style="position: relative;">
<div style="text-align:center;margin-top: 100px;font-size: 20px;">
<strong>没有权限strong>
div>
body>
html>
在web-admin
项目中的resources/spring/spring-mvc.xml
中新增
<mvc:view-controller path="/auth" view-name="frame/auth"/>
在web-admin
项目中创建com.atguigu.config.AtguiguAccessDeniedHandler
类
public class AtguiguAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.sendRedirect("/auth");
}
}
修改web-admin
项目中的com.atguigu.config.WebSecurityConfig
类
@Override
protected void configure(HttpSecurity http) throws Exception {
//允许iframe嵌套显示
http.headers().frameOptions().disable();
http
.authorizeRequests()
.antMatchers("/static/**","/login").permitAll() //允许匿名用户访问的路径
.anyRequest().authenticated() // 其它页面全部需要验证
.and()
.formLogin()
.loginPage("/login") //用户未登录时,访问任何需要权限的资源都转跳到该路径,即登录页面,此时登陆成功后会继续跳转到第一次访问的资源页面(相当于被过滤了一下)
.defaultSuccessUrl("/") //登录认证成功后默认转跳的路径,意思时admin登录后也跳转到/user
.and()
.logout()
.logoutUrl("/logout") //退出登陆的路径,指定spring security拦截的注销url,退出功能是security提供的
.logoutSuccessUrl("/login");//用户退出后要被重定向的url
//关闭跨域请求伪造
http.csrf().disable();
//指定自定义的访问拒绝处理器
http.exceptionHandling().accessDeniedHandler(new AtguiguAccessDeniedHandler());
}
上面我们完成了controller层方法的权限,现在我们要控制页面按钮的权限,如:角色管理上面只有查看权限,那么页面新增、修改、删除、分配权限按都不显示。
怎么实现呢?其实Spring Security已经给我们封装好了标签库,我们直接使用即可。
pom.xml
<thymeleaf-springsecurity5.version>3.0.4.RELEASEthymeleaf-springsecurity5.version>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
<version>${thymeleaf-springsecurity5.version}version>
dependency>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
dependency>
修改web-admin
项目的resources/spring/spring-mvc.xml
配置文件,在Thymeleaf的模板引擎配置spring security 标签支持
<bean id="templateEngine" class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver" ref="templateResolver">property>
<property name="additionalDialects">
<set>
<bean class="org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect" />
set>
property>
bean>
① 在html文件里面声明使用spring-security标签
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
② 在相关按钮上使用标签
<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>
③ 页面完整代码
DOCTYPE html>
<html lang="en"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
xmlns:th="http://www.thymeleaf.org">
<head th:include="common/head :: head">head>
<body class="gray-bg">
<form id="ec" th:action="@{/role}" method="get">
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-content">
<table class="table form-table margin-bottom10">
<tr>
<td>
<input type="text" name="roleName" th:value="${#maps.containsKey(conditions, 'roleName')} ? ${conditions.roleName} : ''" placeholder="角色名称" class="input-sm form-control"/>
td>
tr>
table>
<div>
<button type="button" class="btn btn-sm btn-primary" onclick="javascript:document.forms.ec.pageNum.value=1;document.forms.ec.submit();">搜索button>
<button type="button" onclick="addRole()"
sec:authorize="hasAuthority('role.create')"
class="btn btn-sm btn-primary">新增button>
<button type="button" id="loading-example-btn" onclick="javascript:window.location.reload();" class="btn btn-white btn-sm">刷新button>
div>
<table class="table table-striped table-bordered table-hover dataTables-example">
<thead>
<tr>
<th>序号th>
<th>角色名称th>
<th>角色编码th>
<th>描述th>
<th>创建时间th>
<th>操作 th>
tr>
thead>
<tbody>
<tr class="gradeX" th:each="item,it : ${page.list}">
<td class="text-center" th:text="${it.count}">11td>
<td th:text="${item.roleName}">22td>
<td th:text="${item.roleCode}">33td>
<td th:text="${item.description}">33td>
<td th:text="${#dates.format(item.createTime,'yyyy-MM-dd HH:mm:ss')}" >33td>
<td class="text-center">
<a class="edit" th:attr="data-id=${item.id}"
sec:authorize="hasAuthority('role.edit')"
onclick="editRole()" th:href="@{/role/findById/}+${item.id}">修改a>
<a class="delete"
sec:authorize="hasAuthority('role.delete')"
th:attr="data-id=${item.id}" onclick="deleteRole()" th:href="@{/role/delete/}+${item.id}">删除a>
<a class="assgin"
sec:authorize="hasAuthority('role.assgin')"
th:attr="data-id=${item.id}">分配权限a>
td>
tr>
tbody>
table>
<div class="row" th:include="common/pagination :: pagination">div>
div>
div>
div>
div>
div>
form>
<script th:inline="javascript">
function addRole(){
opt.openWin("/role/create","新增",630,430)
}
function editRole(){
//阻止默认
event.preventDefault()
opt.openWin(event.target.href,'修改',580,430);
}
function deleteRole(){
//阻止默认
event.preventDefault()
opt.confirm(event.target.href)
}
$(".assgin").on("click",function () {
var id = $(this).attr("data-id");
opt.openWin("/role/assignShow/"+id,'修改',580,430);
});
script>
body>
html>
④ 测试
如果用户没有相应的权限,那么按钮会消失