Spring Security
01 - 扫描包
修改工程中的springmvc.xml文件,修改dubbo批量扫描的包
路径
<!‐‐批量扫描‐‐>
<dubbo:annotation package="com.ittest" />
02 - 引入配置文件
在springmvc.xml中添加
<import resource="spring-security.xml"></import>
在工程中提供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">
<!--配置哪些资源匿名可以访问(不登录也可以访问)-->
<!--<security:http security="none" pattern="/pages/a.html"></security:http>
<security:http security="none" pattern="/pages/b.html"></security:http>-->
<!--<security:http security="none" pattern="/pages/**"></security:http>-->
<security:http security="none" pattern="/login.html"></security:http>
<security:http security="none" pattern="/css/**"></security:http>
<security:http security="none" pattern="/img/**"></security:http>
<security:http security="none" pattern="/js/**"></security:http>
<security:http security="none" pattern="/plugins/**"></security:http>
<!--
auto-config:自动配置,如果设置为true,表示自动应用一些默认配置,比如框架会提供一个默认的登录页面
use-expressions:是否使用spring security提供的表达式来描述权限
-->
<security:http auto-config="true" use-expressions="true">
<security:headers>
<!--设置在页面可以通过iframe访问受保护的页面,默认为不允许访问
DENY:浏览器拒绝当前页面加载任何Frame页面
SAMEORIGIN:frame页面的地址只能为同源域名下的页面
ALLOW-FROM:origin为允许frame加载的页面地址。
-->
<security:frame-options policy="SAMEORIGIN"></security:frame-options>
</security:headers>
<!--配置拦截规则,/** 表示拦截所有请求-->
<!--
pattern:描述拦截规则
asscess:指定所需的访问角色或者访问权限
-->
<!--只要认证通过就可以访问-->
<security:intercept-url pattern="/pages/**" access="isAuthenticated()" />
<!--如果我们要使用自己指定的页面作为登录页面,必须配置登录表单.页面提交的登录表单请求是由框架负责处理-->
<!--
login-page:指定登录页面访问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"></security:form-login>
<!--
csrf:对应CsrfFilter过滤器
disabled:是否启用CsrfFilter过滤器,如果使用自定义登录页面需要关闭此项,否则登录操作会被禁用(403)
-->
<security:csrf disabled="true"></security:csrf>
<!--
logout:退出登录
logout-url:退出登录操作对应的请求路径
logout-success-url:退出登录后的跳转页面
-->
<security:logout logout-url="/logout.do"
logout-success-url="/login.html" invalidate-session="true"/>
</security:http>
<!--配置认证管理器-->
<security:authentication-manager>
<!--配置认证提供者-->
<security:authentication-provider user-service-ref="springSecurityUserService">
<!--
配置一个具体的用户,后期需要从数据库查询用户
<security:user-service>
<security:user name="admin" password="{noop}1234" authorities="ROLE_ADMIN"/>
</security:user-service>
-->
<!--指定度密码进行加密的对象-->
<security:password-encoder ref="passwordEncoder"></security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
<!--配置密码加密对象-->
<bean id="passwordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
<!--开启注解方式权限控制-->
<security:global-method-security pre-post-annotations="enabled" />
</beans>
以下资源需要放行:
<security:http security="none" pattern="/login.html"></security:http> <security:http security="none" pattern="/css/**"></security:http> <security:http security="none" pattern="/img/**"></security:http> <security:http security="none" pattern="/js/**"></security:http> <security:http security="none" pattern="/plugins/**"></security:http>
所有页面认证通过就能访问,细粒度的权限控制在接口层controller中通过注解方式做:
<!--只要认证通过就可以访问-->
<security:intercept-url pattern="/pages/**" access="isAuthenticated()" />
登录完之后的首页更改为:
/pages/main.html
认证管理器就不需要注入bean了,只需要进行应用,名称为类名首字母小写
<security:authentication-provider user-service-ref="springSecurityUserService">
03 - iframe放行
数据库中的密码有误,admin密码应该为:
admin/admin
$2a 10 10 10QjWSVpt35GMwq9sPSb6beu2Ctp5o5iQyZ/kbpHJlgkA.rpkEqtfLS
在<security:http auto-config=“true” use-expressions=“true”>中添加
<security:headers>
<!--设置在页面可以通过iframe访问受保护的页面,默认为不允许访问-->
<security:frame-options policy="SAMEORIGIN"></security:frame-options>
</security:headers>
由于页面框架右边是使用iframe进行内嵌页面展示,需要添加策略,默认为DENY
DENY:浏览器拒绝当前页面加载任何Frame页面
SAMEORIGIN:frame页面的地址只能为同源域名下的页面
ALLOW-FROM:origin为允许frame加载的页面地址。必须配置strategy属性和value属性。否则项目启动报错。
关闭策略:
<security:frame-options disabled=“true”/>
04 - 测试用 html
处理登录的异常,给用户提示:
<!DOCTYPE html>
<html>
<head>
<!-- 页面meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="keywords" content="">
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
<!-- 引入样式 -->
<link rel="stylesheet" href="plugins/elementui/index.css">
<link rel="stylesheet" href="css/login.css">
<!-- 引入组件库 -->
<script src="js/vue.js"></script>
<script src="plugins/elementui/index.js"></script>
<script src="js/axios-0.18.0.js"></script>
<script>
function getUrlParam(paraName) {
var url = document.location.toString();
//alert(url);
var arrObj = url.split("?");
if (arrObj.length > 1) {
var arrPara = arrObj[1].split("&");
var arr;
for (var i = 0; i < arrPara.length; i++) {
arr = arrPara[i].split("=");
if (arr != null && arr[0] == paraName) {
return arr[1];
}
}
return "";
}
else {
return "";
}
}
var param = getUrlParam('error')
if(param){
axios.get("/user/loginerror.do").then(res => {
alert(res.data)
})
}
</script>
</head>
<body class="hold-transition skin-purple sidebar-mini">
<div id="app">
<div class="login-container">
<div class="loginBox">
<form method="post" class="login-form" action="/login.do" label-position="left">
<div class="title-container">
<div class="logoInfo clearfix">
<em class="logo"></em>
</div>
</div>
<div>
<span class="svg-container svg-container_login">
<span class="user"></span>
</span>
<input type="text" name="username" placeholder="请输入用户名" />
</div>
<div>
<span class="svg-container">
<span class="username"></span>
</span>
<input type="password" name="password" placeholder="请输入密码"/>
</div>
<input type="submit" style="width:100%;margin-bottom:30px;" value="登录"></input>
</form>
</div>
</div>
</div>
</body>
</html>
service:
package com.ittest.service;
import com.alibaba.dubbo.config.annotation.Reference;
import com.ittest.backend.pojo.UserEnhancher;
import com.ittest.exception.MyUsernameNotFoundException;
import com.ittest.pojo.Permission;
import com.ittest.pojo.Role;
import com.ittest.pojo.User;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Component
public class SpringSecurityUserService implements UserDetailsService {
//使用dubbo通过网络远程调用服务提供方获取数据库中的用户信息
@Reference
private UserService userService;
//根据用户名查询数据库获取用户信息
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if(user == null){
//用户名不存在;
throw new MyUsernameNotFoundException("用户名不存在");
}
List<GrantedAuthority> list = new ArrayList<>();
//动态为当前用户授权 [1[4,5,6],2[7,8,9],3[10]]
Set<Role> roles = user.getRoles();
for (Role role : roles) {
//遍历角色集合,为用户授予角色ROLE_XXX
list.add(new SimpleGrantedAuthority(role.getKeyword()));
Set<Permission> permissions = role.getPermissions();
for (Permission permission : permissions) {
//遍历权限集合,为用户授权 CHECKITEM_ADD
list.add(new SimpleGrantedAuthority(permission.getKeyword()));
}
}
//org.springframework.security.core.userdetails.User
User securityUser = new User(username,user.getPassword(),list);
return securityUser;
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("admin"));
}
}
异常:
package com.ittest.exception;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class MyUsernameNotFoundException extends AuthenticationException {
private static final long serialVersionUID = 1L;
public MyUsernameNotFoundException(String msg) {
super(msg);
}
public MyUsernameNotFoundException(String msg, Throwable t) {
super(msg, t);
}
}
controller:
package com.ittest.controller;
import com.ittest.backend.pojo.UserEnhancher;
import com.ittest.constant.MessageConstant;
import com.ittest.entity.Result;
import com.ittest.exception.MyUsernameNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* 用户操作
*/
@RestController
@RequestMapping("/user")
public class UserController {
//获得当前登录用户的用户名
@RequestMapping("/getUsername")
public Result getUsername(HttpServletRequest request){
//当Spring security完成认证后,会将当前用户信息保存到框架提供的上下文对象
request.getHeader("AuthToken");
UserEnhancher user = (UserEnhancher) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println(user);
if(user != null){
String username = user.getRealname();
return new Result(true, MessageConstant.GET_USERNAME_SUCCESS,username);
}
return new Result(false, MessageConstant.GET_USERNAME_FAIL);
}
//获得错误信息
@RequestMapping("/loginerror")
public String loginerror(HttpServletRequest request, HttpServletResponse response){
HttpSession session = request.getSession();
AuthenticationException spring_security_last_exception = (AuthenticationException)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
/**
* 用户名不存在:UsernameNotFoundException;
* 密码错误:BadCredentialException;
* 帐户被锁:LockedException;
* 帐户未启动:DisabledException;
* 密码过期:CredentialExpiredException;
*/
if(spring_security_last_exception instanceof BadCredentialsException){
return "密码错误";
}
return spring_security_last_exception.getMessage();
}
}
05 - 接口权限控制
在CheckItemController中更改代码:
//删除检查项
@PreAuthorize("hasAuthority('CHECKITEM_DELETE')")//权限校验
@RequestMapping("/delete")
public Result delete(Integer id){
try{
checkItemService.deleteById(id);
}catch (Exception e){
e.printStackTrace();
//服务调用失败
return new Result(false, MessageConstant.DELETE_CHECKITEM_FAIL);
}
return new Result(true, MessageConstant.DELETE_CHECKITEM_SUCCESS);
}
在数据库中将t_role_permission表中role_id是2,permission_id是2的数据删除
使用admin/admin登录之后,删除检查项时不会报没有权限的提示
使用xiaoming/1234登录,删除检查项会没有提示,因为我们还没有把没有权限的错误返回到前端进行展示
06 - html 设置
返回非200的返回码时,需要进行catch处理
// 删除
handleDelete(row) {//row其实是一个json对象,json对象的结构为{"age":"0-100","attention":"无","code":"0011","id":38,"name":"白细胞计数","price":10.0,"remark":"白细胞计数","sex":"0","type":"2"}
//alert(row.id);
this.$confirm("你确定要删除当前数据吗?","提示",{//确认框
type:'warning'
}).then(()=>{
//用户点击确定按钮,发送ajax请求,将检查项ID提交到Controller进行处理
axios.get("/checkitem/delete.do?id=" + row.id).then((res) => {
if(res.data.flag){
//执行成功
//弹出成功提示信息
this.$message({
type:'success',
message:res.data.message
});
//重新进行分页查询
this.findPage();
}else{
//执行失败
this.$message.error(res.data.message);
}
}).catch((r)=>{
this.showMessage(r);
});
}).catch(()=>{
this.$message({
type:'info',
message:'操作已取消'
});
});
}
我们可以封装一个showMessage方法进行复用
showMessage(r){
if(r == 'Error: Request failed with status code 403'){
//权限不足
this.$message.error('无访问权限');
return;
}else{
this.$message.error('未知错误');
return;
}
},
备注:其实这种方式不是特别规范,如果将来spring security框架返回的信息变化了,就无法触发无访问权限。
showMessage(r){
if(r.response.status == 403){
//权限不足
this.$message.error('无访问权限');
return;
}else{
this.$message.error('未知错误');
return;
}
},
使用http返回码进行判断,如果是403就代表无访问权限