1. 在项目中应用Spring Security
前面我们已经学习了Spring Security框架的使用方法,我们就需要将Spring Security框架应用到后台系统中进行权限控制(多应用于后台系统),其本质就是认证和授权。
要进行认证和授权需要前面课程中提到的权限模型涉及的7张表支撑,因为用户信息、权限信息、菜单信息、角色信息、关联信息等都保存在这7张表中,也就是这些表中的数据是我们进行认证和授权的依据。所以在真正进行认证和授权之前需要对这些数据进行管理,即我们需要开发如下一些功能:
- 权限数据管理(增删改查)
- 菜单数据管理(增删改查)
- 角色数据管理(增删改查、角色关联权限、角色关联菜单)
- 用户数据管理(增删改查、用户关联角色)
鉴于本文长度,不再实现这些数据管理的代码开发。
- 表结构
1.1 导入Spring Security环境
第一步:在health_parent父工程的pom.xml中导入Spring Security的maven坐标
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
第二步:在health_backend工程的web.xml文件中配置用于整合Spring Security框架的过滤器DelegatingFilterProxy
;
<!--委派过滤器,用于整合其他框架-->
<filter>
<!--整合spring security时,此过滤器的名称固定springSecurityFilterChain-->
<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>
注意:在使用Spring Security时,<filter-name>内必须命名springSecurityFilterChain,否则运行将会抛出异常
1.2 实现认证和授权
第一步:在health_backend工程中按照Spring Security框架要求提供SpringSecurityUserService
,并且实现UserDetailsService
接口
package com.itheiheihei.serivce;
import com.alibaba.dubbo.config.annotation.Reference;
import com.itheiheihei.pojo.Permission;
import com.itheiheihei.pojo.Role;
import com.itheiheihei.pojo.User;
import com.itheiheihei.service.UserService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* Spring Security权限控制
*
* @author 嘿嘿嘿1212
* @version 1.0
* @date 2019/10/24 20:18
*/
@Component
public class StringSecurityUserService implements UserDetailsService {
//所有dubbo通过网络远程调用服务提供方获取数据库中的用户信息
@Reference
private UserService userService;
/**
* 根据用户名查询数据库获取用户信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
//用户名不存在
return null;
}
//动态为当前用户授权
Set<Role> roles = user.getRoles();
List<GrantedAuthority> list = new ArrayList<>();
for (Role role : roles) {
//授予角色
list.add(new SimpleGrantedAuthority(role.getKeyword()));
Set<Permission> permissions = role.getPermissions();
for (Permission permission : permissions) {
//授权
list.add(new SimpleGrantedAuthority(permission.getKeyword()));
}
}
return new org.springframework.security.core.userdetails.User(username, user.getPassword(), list);
}
}
注意:返回Security的User时,在5.0.0版本以上,建议在密码前添加{bcrypt}
即
//改User为Security的User
User securityUser = new User(username, "{bcrypt}"+password, list);
第二步:创建UserService服务接口、服务实现类、Dao接口、Mapper映射文件等
- UserService服务接口
package com.itheiheihei.service;
import com.itheiheihei.pojo.User;
/**
* @author 嘿嘿嘿1212
* @version 1.0
* @date 2019/10/24 20:24
*/
public interface UserService {
/**
* 根据用户名称查询用户
*
* @param username 用户名
* @return
*/
public User findByUsername(String username);
}
- 服务实现类
package com.itheiheihei.service.impl;
import com.alibaba.dubbo.config.annotation.Service;
import com.itheiheihei.dao.PermissionDao;
import com.itheiheihei.dao.RoleDao;
import com.itheiheihei.dao.UserDao;
import com.itheiheihei.pojo.Permission;
import com.itheiheihei.pojo.Role;
import com.itheiheihei.pojo.User;
import com.itheiheihei.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
/**
* @author 嘿嘿嘿1212
* @version 1.0
* @date 2019/10/24 20:55
*/
@Service(interfaceClass = UserService.class)
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
@Autowired
private PermissionDao permissionDao;
@Override
public User findByUsername(String username) {
User user = userDao.findByUsername(username);
if (user == null) {
return null;
}
//根据用户id查询对应角色
Set<Role> roles = roleDao.findByUserId(user.getId());
for (Role role : roles) {
Integer id = role.getId();
//根据角色id查询关联对应的权限
Set<Permission> permissions = permissionDao.findByRoleId(id);
//进行角色关联权限
role.setPermissions(permissions);
}
//用户关联角色
user.setRoles(roles);
return user;
}
}
注意:角色需要关联了权限,再关联给用户
- Dao接口
package com.itheiheihei.dao;
import com.itheiheihei.pojo.User;
public interface UserDao {
public User findByUsername(String username);
}
package com.itheiheihei.dao;
import com.itheiheihei.pojo.Role;
import java.util.Set;
public interface RoleDao {
public Set<Role> findByUserId(int id);
}
package com.itheiheihei.dao;
import com.itheiheihei.pojo.Permission;
import java.util.Set;
public interface PermissionDao {
public Set<Permission> findByRoleId(int roleId);
}
- Mapper映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.itheiheihei.dao.UserDao">
<select id="findByUsername" parameterType="string" resultType="com.itheiheihei.pojo.User">
select * from t_user
where username=#{username}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.itheiheihei.dao.RoleDao">
<select id="findByUserId" parameterType="int" resultType="com.itheiheihei.pojo.Role">
select * from t_role r,t_user_role ur
where r.id=ur.role_id and ur.user_id=#{userid}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.itheiheihei.dao.PermissionDao">
<select id="findByRoleId" parameterType="int" resultType="com.itheiheihei.pojo.Permission">
select p.* from t_permission p,t_role_permission rp
where p.id=rp.permission_id and rp.role_id=#{roleId}
</select>
</mapper>
注意:可以使用<collection>
进行级联查询(表与表)
第三步:修改health_backend工程中的springmvc.xml
文件,修改dubbo批量扫描的包路径
<!--批量扫描-->
<dubbo:annotation package="com.itheiheihei" />
注意:此处原来扫描的包为com.itheiheihei.controller
,现在改为com.itheiheihei
包的目的是需要将我们上面定义的SpringSecurityUserService
也扫描到,因为在SpringSecurityUserService
的loadUserByUsername
方法中需要通过dubbo远程调用名称为UserService的服务。
第四步:在health_backend工程中提供spring-security.xml
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--
http:用于定义相关权限控制
security:是否开启安全保护
pattern:拦截拦截,可以使用通配符表达式
-->
<!--<security:http security="none" pattern="/pages/**"/>-->
<security:http security="none" pattern="/login.html"/>
<security:http security="none" pattern="/css/**"/>
<security:http security="none" pattern="/img/**"/>
<security:http security="none" pattern="/js/**"/>
<security:http security="none" pattern="/plugins/**"/>
<!--
http:用于定义相关权限控制
auto-config:是否自动配置
设置true时框架会提供默认的一些设置,例如提供的登陆页面,登出处理等
设置false时需要显示提供登陆表单配置,否则会报
use-expressions:用于指定intercept-url中access属性是否使用表达式
-->
<security:http auto-config="true" use-expressions="true">
<!--设置在页面可以通过iframe访问受保护的页面,默认为不允许访问-->
<security:headers>
<security:frame-options policy="SAMEORIGIN"/>
</security:headers>
<!--
intercept-url:定义一个拦截规则
pattern:对那些url进行权限控制
access:在请求对应的URL时需要什么权限,默认配置时它应该是一个以逗号分隔的角色列表
,请求的用户只需要拥有其中的一个角色就能完成访问对应的URL
-->
<!--只要认证通过就可以访问-->
<security:intercept-url pattern="/pages/**" access="isAuthenticated()"/>
<!--如果我们要使用自己指定的页面作为登录页面,必须配置登录表单,页面提交的登录表单请求是由框架负责处理-->
<!--
login-page:指定登录页面
username-parameter:绑定表单提交的账号
password-parameter:绑定表单提交的密码
login-processing-url:登录处理的URL
default-target-url:默认目标的URL
authentication-failure-url:权限验证失败跳转的URL
-->
<security:form-login
login-page="/login.html"
username-parameter="username"
password-parameter="password"
login-processing-url="/login.do"
default-target-url="/pages/main.html"
authentication-failure-url="/login.html"
/>
<!--
csrf:对应CsrfFilter过滤器
disabled:是否启用CsrfFilter过滤器,如果使用自定义登录页面需要关闭,
否则登录操作会被禁止报错(403)
Spring security默认登录页面存在隐藏域进行验证是否是默认页面,而使用自定义时没有,这时security将会认为不安全
-->
<security:csrf disabled="true"/>
<security:logout logout-url="/logout.do" logout-success-url="/login.html" invalidate-session="true"/>
</security:http>
<!--authentication-manager:认证管理器,用于处理认证操作-->
<security:authentication-manager>
<!--authentication-provider:认证提供者,执行具体的认证逻辑-->
<security:authentication-provider user-service-ref="stringSecurityUserService">
<!--
user-service:用于获取用户信息,提供给authentication
provider:进行认证
<security:user-service>
user:定义用户信息,可以指定用户名,密码,角色,后期可以改为从数据库查询用户信息
(noop):表示当前使用的密码为明文
<security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN"/>
</security:user-service>
-->
<security:password-encoder ref="bCryptPasswordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
<!--载入加密对象-->
<bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!--开启Spring注解-->
<context:annotation-config/>
<!--开启security注解-->
<security:global-method-security pre-post-annotations="enabled"/>
</beans>
注意:需要开启Spring的注解和注解扫描,因为需要使用依赖注入等注解
,由于在spring-mvc.xml中已经配置了注解扫描,所以这里无需配置.
第五步:在springmvc.xml文件中引入spring-security.xml文件
<import resource="spring-security.xml"></import>
知识补充:在Spring 3.1后,同一标签注解扫描不再覆盖,例如<context:component-scan>
,将会并存.导入的文件将会一第一个导入为准(同一内容),例如log4j.properties,XML配置注入等,配置优先于注解
第六步:在Controller的方法上加入权限控制注解,此处以CheckItemController为例
package com.itheiheihei.controller;
import com.alibaba.dubbo.config.annotation.Reference; import com.itheiheihei.constant.MessageConstant;
import com.itheiheihei.constant.PermissionConstant; import com.itheiheihei.entity.PageResult;
import com.itheiheihei.entity.QueryPageBean; import com.itheiheihei.entity.Result;
import com.itheiheihei.exception.CustomException; import com.itheiheihei.pojo.CheckItem; import com.itheiheihei.pojo.Member;
import com.itheiheihei.service.CheckItemService;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
import java.util.List;
/**
* 体检检查项管理
*/
@RestController
@RequestMapping("/checkitem")
public class CheckItemController {
@Reference
private CheckItemService checkItemService;
//分页查询
@PreAuthorize("hasAuthority('CHECKITEM_QUERY')")//权限校验
@RequestMapping("/findPage")
public PageResult findPage(@RequestBody QueryPageBean queryPageBean){ PageResult pageResult = checkItemService.pageQuery(
queryPageBean.getCurrentPage(),
queryPageBean.getPageSize(),
queryPageBean.getQueryString());
return pageResult;
}
//删除
@PreAuthorize("hasAuthority('CHECKITEM_DELETE')")//权限校验
@RequestMapping("/delete")
public Result delete(Integer id){
try {
checkItemService.delete(id);
}catch (RuntimeException e){
return new Result(false,e.getMessage());
}catch (Exception e){
return new Result(false, MessageConstant.DELETE_CHECKITEM_FAIL);
}
return new Result(true,MessageConstant.DELETE_CHECKITEM_SUCCESS);
}
//新增
@PreAuthorize("hasAuthority('CHECKITEM_ADD')")//权限校验
@RequestMapping("/add")
public Result add(@RequestBody CheckItem checkItem){
try {
checkItemService.add(checkItem);
}catch (Exception e){
return new Result(false,MessageConstant.ADD_CHECKITEM_FAIL);
}
return new Result(true,MessageConstant.ADD_CHECKITEM_SUCCESS);
}
//编辑
@PreAuthorize("hasAuthority('CHECKITEM_EDIT')")//权限校验
@RequestMapping("/edit")
public Result edit(@RequestBody CheckItem checkItem){
try {
checkItemService.edit(checkItem);
}catch (Exception e){
return new Result(false,MessageConstant.EDIT_CHECKITEM_FAIL);
}
return new Result(true,MessageConstant.EDIT_CHECKITEM_SUCCESS);
}
}
第七步:修改页面,没有权限时提示信息设置,此处以checkitem.html中的handleDelete方法为例
//权限不足提示
showMessage(r){
if(r == 'Error: Request failed with status code 403'){
//权限不足
this.$message.error('无访问权限');
return;
}else{
this.$message.error('未知错误');
return;
}
}
//删除
handleDelete(row) {
this.$confirm('此操作将永久当前数据,是否继续?', '提示',
{type: 'warning' }).then(()=>{
//点击确定按钮执行此代码
axios.get("/checkitem/delete.do?id=" + row.id).then((res)=> {
if(!res.data.flag){
//删除失败
this.$message.error(res.data.message);
}else{
//删除成功
this.$message(
{
message: res.data.message,
type: 'success'
});
this.findPage();
}
}).catch((r)=>{
this.showMessage(r);
});
}).catch(()=> {
//点击取消按钮执行此代码
this.$message('操作已取消');
});
}
1.3 显示用户名
前面我们已经完成了认证和授权操作,如果用户认证成功后需要在页面展示当前用户的用户名。Spring Security在认证成功后会将用户信息保存到框架提供的上下文对象中,所以此处我们就可以调用Spring Security框架提供的API获取当前用户的username并展示到页面上。
实现步骤:
第一步:在main.html页面中修改,定义username模型数据基于VUE的数据绑定展示用户名,发送ajax请求获取username
<script>
new Vue({
el: '#app',
data: {
username: ''
},
created() {
axios.get("/user/getUsername.do").then((resp) => {
if (resp.data.flag) {
this.username = resp.data.data;
} else {
this.$message.error(resp.data.message);
}
})
}
});
$(function () {
var wd = 200;
$(".el-main").css('width', $('body').width() - wd + 'px');
});
</script>
<div class="avatar-wrapper">
<img src="../img/user2-160x160.jpg" class="user-avatar"> <!--展示用户名-->
{{username}}
</div>
第二步:创建UserController并提供getUsername方法
package com.itheiheihei.controller;
import com.itheiheihei.constant.MessageConstant;
import com.itheiheihei.entity.Result;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户相关操作
*
* @author 嘿嘿嘿1212
* @version 1.0
* @date 2019/10/25 20:10
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getUsername")
public Result getUsername() {
//获取security的User对象
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
//是否存在
if (user != null) {
String username = user.getUsername();
return new Result(true, MessageConstant.GET_USERNAME_SUCCESS, username);
}
return new Result(false, MessageConstant.GET_USERNAME_FAIL);
}
}
通过debug调试可以看到Spring Security框架在其上下文中保存的用户相关信息:
1.4 用户退出
第一步:在main.html中提供的退出菜单上加入超链接
<el-dropdown-item divided>
<span style="display:block;"><a href="/logout.do">退出</a></span> </el-dropdown-item>
第二步:在spring-security.xml文件中配置
<!--
logout:退出登录
logout-url:退出登录操作对应的请求路径
logout-success-url:退出登录后的跳转页面
-->
<security:logout logout-url="/logout.do"
logout-success-url="/login.html"
invalidate-session="true"/>