在沒(méi)有用權(quán)限框架之前田弥,我們做權(quán)限控制的時(shí)候一般思路是這樣的:
1.登錄的時(shí)候從數(shù)據(jù)庫(kù)中檢索user所擁有的privileges存在內(nèi)存中
2.在用戶發(fā)出請(qǐng)求的時(shí)候陪捷,將請(qǐng)求信息與用戶所擁有的權(quán)限對(duì)比能庆。
spring security也有這種實(shí)現(xiàn)渠啊,然而我們要做的就是:
自定義過(guò)濾器,代替原有的FilterSecurityInterceptor過(guò)濾器锁孟,實(shí)現(xiàn)
UserDetailsService (儲(chǔ)存用戶所有角色)
InvocationSecurityMetadataSourceService(訪問(wèn)資源所需要的角色集合)
AccessDecisionManager(判斷用戶請(qǐng)求的資源 是否能通過(guò))
思路很明了看看實(shí)現(xiàn):
說(shuō)明:
用了jpa 當(dāng)時(shí)參考了一些資料,走了很多彎路發(fā)現(xiàn)沒(méi)有很大作用,
還是習(xí)慣自己寫sql允耿,所以這里只是用來(lái)創(chuàng)建表格,反正以后擴(kuò)展crud會(huì)用到扒怖。
真正做持久化的是mybatis较锡,這兩個(gè)整合在一起了,只是多創(chuàng)建了一個(gè)接口而已盗痒。
準(zhǔn)備工作
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!-- jpa -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
# server
server:
port: 8888
spring:
# dataSource
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/nul_blog?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: admin
password: root
# jpa
jpa:
hibernate:
ddl-auto: update
show-sql: true
database: mysql
# thymeleaf
thymeleaf:
prefix: classpath:/templates/
cache: false
suffix: .html
# mybatis
mybatis:
mapper-locations: mappers/*.xml
一.用戶 角色 權(quán)限 三張表
很普通的bean蚂蕴,user類并沒(méi)有繼承userdetails
@Entity
@Table(name = "sys_user")
public class SysUser implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
@NotNull
@Column(unique = true)
private String username;
@NotNull
private String password;
@ManyToMany
@JoinTable(name = "sys_user_role",joinColumns = {@JoinColumn(name = "sys_user_id")},inverseJoinColumns={@JoinColumn(name = "sys_role_id")})
private List<SysRole> roles;
//... getter and settter
}
@Entity
@Table(name = "sys_role")
public class SysRole implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
@NotNull
private String name;
@ManyToMany
@JoinTable(name = "sys_role_permission",
joinColumns = {@JoinColumn(name = "sys_role_id")},
inverseJoinColumns={@JoinColumn(name = "sys_permission_id")})
private List<SysPermission> permissions;
//... getter and settter
}
@Entity
@Table(name = "sys_permission")
public class SysPermission implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
@NotNull
private Long pid;
@NotNull
private String name;
@NotNull
private String url;
@NotNull
private String description;
//... getter and settter
}
public class SysRolePermisson {
//角色
private String roleId;
private String roleName;
//權(quán)限
private String permissionId;
private String url;
//... getter and settter
}
關(guān)于數(shù)據(jù)的設(shè)計(jì)我是這樣想的:
基礎(chǔ)用戶和高級(jí)用戶區(qū)別是擁有多的權(quán)限
所以多的權(quán)限就把他賦給另個(gè)角色
所以一個(gè)用戶最少擁有基礎(chǔ)用戶的權(quán)限
其次才擁有高級(jí)用戶的權(quán)限
@Mapper
public interface SysUserMapper {
/**
* 通過(guò)username查找 user
* username是唯一的前提
*
* @param username
* @return SysUser
*/
SysUser findUserByUsername(String username);
/**
* 通過(guò)用戶名 查找·
* @param username
* @return List<SysRole>
*/
List<SysRole> findRolesByUsername(String username);
/**
* 通過(guò)用戶名 查找權(quán)限
* @param username
* @return List<SysPermission>
*/
List<SysPermission> findPermissionsByUsername(String username);
List<SysRolePermisson> findAllRolePermissoin();
}
<?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.sandnul.repository.SysUserMapper">
<select id="findUserByUsername" resultType="com.sandnul.domain.SysUser">
select id,password,username from SYS_user WHERE username = #{username}
</select>
<select id="findPermissionsByUsername" resultType="com.sandnul.domain.SysPermission">
select sp.*
from sys_user su
left join sys_user_role sur on su.id = sur.sys_user_id
left join sys_role_permission srp on sur.sys_role_id = srp.sys_role_id
left join sys_permission sp on srp.sys_permission_id = sp.id
where su.username = #{username}
</select>
<select id="findRolesByUsername" resultType="com.sandnul.domain.SysRole">
select sr.*
from sys_user su
left join sys_user_role sur on su.id = sur.sys_user_id
left join sys_role sr on sur.sys_role_id = sr.id
where su.username = #{username}
</select>
<select id="findAllRolePermissoin" resultType="com.sandnul.domain.SysRolePermisson">
select sr.id roleId ,sr.name roleName,sp.id permissionId,sp.url
from sys_role_permission srp
left join sys_role sr on sr.id = srp.sys_role_id
left join sys_permission sp on srp.sys_permission_id = sp.id
</select>
</mapper>
public interface SysUserRepository extends JpaRepository<SysUser, Long>{
}
以下就是思路的實(shí)現(xiàn)
二、替換原先的攔截器
說(shuō)實(shí)話我不知道這個(gè)攔截器和原先的攔截器有什么不同俯邓,我去掉后發(fā)現(xiàn)權(quán)限亂了
骡楼,估計(jì)還要讀一些源碼吧。
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter{
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//執(zhí)行下一個(gè)攔截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
三稽鞭、重寫UserDetailsService,登錄認(rèn)證
認(rèn)證是由 AuthenticationManager 來(lái)管理的鸟整,但是真正進(jìn)行認(rèn)證的是 AuthenticationManager 中定義的 AuthenticationProvider。Spring Security 默認(rèn)會(huì)使用DaoAuthenticationProvider朦蕴。DaoAuthenticationProvider 在進(jìn)行認(rèn)證的時(shí)候需要一個(gè) UserDetailsService 來(lái)獲取用戶的信息 UserDetails篮条。改變認(rèn)證的方式,就實(shí)現(xiàn) UserDetailsService吩抓,返回我們自己userdetails涉茧。
@Component
public class MyCustomUserService implements UserDetailsService{
@Autowired
private SysUserMapper sysUserMapper;
/**
* 將用的所有角色存儲(chǔ)于用戶信息中
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserMapper.findUserByUsername(username);
if(user == null)
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
//獲取所有請(qǐng)求的url
//List<SysPermission> sysPermissions = sysUserMapper.findPermissionsByUsername(user.getUsername());
List<SysRole> sysRoles = sysUserMapper.findRolesByUsername(user.getUsername());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (SysRole sysRole : sysRoles) {
//封裝用戶信息和角色信息 到 SecurityContextHolder全局緩存中
grantedAuthorities.add(new SimpleGrantedAuthority(sysRole.getName()));
}
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
}
四、實(shí)現(xiàn)FilterInvocationSecurityMetadataSource
作用為了將所有資源和資源對(duì)應(yīng)需要的角色存在map中
用戶請(qǐng)求資源的時(shí)候能夠返回資源所對(duì)應(yīng)的角色集合給
決策器(MyAccessDecisionManager)
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final static Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
/**
* 通過(guò) 實(shí)現(xiàn)UserDetailService 來(lái)進(jìn)行驗(yàn)證
*/
@Autowired
private MyCustomUserService myCustomUserService;
/**
*
* @param auth
* @throws Exception
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
//校驗(yàn)用戶
auth.userDetailsService(myCustomUserService)
//校驗(yàn)密碼
.passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return Md5Util.MD5(String.valueOf(rawPassword));
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(Md5Util.MD5(String.valueOf(rawPassword)));
}
});
}
/**
* 創(chuàng)建自定義的表單
*
* 頁(yè)面琴拧、登錄請(qǐng)求降瞳、跳轉(zhuǎn)頁(yè)面等
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","index","/login","/css/**","/js/**")//允許訪問(wèn)
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")//攔截后get請(qǐng)求跳轉(zhuǎn)的頁(yè)面
.defaultSuccessUrl("/hello")
.permitAll()
.and()
.logout()
.permitAll();
}
}
@Component
public class MyInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
private SysUserMapper sysUserMapper;
/**
* 每一個(gè)資源所需要的角色 Collection<ConfigAttribute>決策器會(huì)用到 不急弄清楚
*/
private static HashMap<String, Collection<ConfigAttribute>> map =null;
//初始化 所有資源 對(duì)應(yīng)的角色
public void loadResourceDefine(){
map = new HashMap<>();
//權(quán)限資源 和 角色對(duì)應(yīng)的表 也就是 角色 權(quán)限中間表
List<SysRolePermisson> rolePermissons = sysUserMapper.findAllRolePermissoin();
//每個(gè)資源 所需要的角色
for (SysRolePermisson rolePermisson : rolePermissons) {
String url = rolePermisson.getUrl();
String roleName = rolePermisson.getRoleName();
ConfigAttribute role = new SecurityConfig(roleName);
if(map.containsKey(url)){
map.get(url).add(role);
}else{
map.put(url,new ArrayList<ConfigAttribute>(){{
add(role);
}});
}
}
}
/**
* 我的理解是 方法會(huì)被調(diào)用,然后返回資源所需要的角色
* @param object
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null)
loadResourceDefine();
//object 中包含用戶請(qǐng)求的request 信息
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
String url = iter.next();
if(new AntPathRequestMatcher(url).matches(request)) {
return map.get(url);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
四蚓胸、決策器
主要的方法返回值是void挣饥,可以想到既然判斷是否有權(quán)通過(guò),
那沒(méi)通過(guò)肯定就是拋出異常沛膳,讓外層捕捉扔枫。
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class);
/**
* 判定 是否含有權(quán)限
* @param authentication CustomUserService.loadUserByUsername() 封裝的用戶信息
* @param object request請(qǐng)求信息
* @param configAttributes InvocationSecurityMetadataSourceService.getAttributes() 中每個(gè)資源可訪問(wèn)的角色集合
* @throws AccessDeniedException
* @throws InsufficientAuthenticationException
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
return;
}
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
needRole = iter.next().getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority().trim())) {
return;
}
}
}
throw new AccessDeniedException("no privilege");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
請(qǐng)求和頁(yè)面
@Controller
public class TestController {
@GetMapping(value = {"/","index"})
public String index(){
return "index";
}
@GetMapping(value = "hello")
public String hello(){
return "hello";
}
@GetMapping(value = "login")
public String login(){
return "login";
}
@GetMapping(value = "admin")
public String admin(Model model){
model.addAttribute("title","標(biāo)題");
model.addAttribute("content","內(nèi)容");
model.addAttribute("extraInfo","你是admin");
return "admin";
}
}
admin.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8"/>
<title sec:authentication="name"></title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}"/>
<style type="text/css">
body {
padding-top: 50px;
}
.starter-template {
padding: 40px 15px;
text-align: center;
}
</style>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Spring Security演示</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a th:href="@{/}">首頁(yè)</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="starter-template">
<h1 th:text="${title}"></h1>
<p class="bg-primary" th:text="${content}"></p>
<div sec:authorize="hasRole('ROLE_ADMIN')">
<p class="bg-info" th:text="${extraInfo}"></p>
</div>
<div sec:authorize="hasRole('ROLE_USER')">
<p class="bg-info">無(wú)更多顯示信息</p>
</div>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="注銷"/>
</form>
</div>
</div>
</body>
</html>
hello.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<h1>hello spring boot with security</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="注銷"/>
</form>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
index
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>登錄</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}"/>
<style type="text/css">
body {
padding-top: 50px;
}
.starter-template {
padding: 40px 15px;
text-align: center;
}
</style>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Spring Security演示</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a th:href="@{/}">首頁(yè)</a></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="starter-template">
<p th:if="${param.logout}" class="bg-warning">已注銷</p>
<p th:if="${param.error}" class="bg-danger" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}">有錯(cuò)誤</p>
<h2>使用賬號(hào)密碼登錄</h2>
<form name="form" th:action="@{/login}" action="/login" method="post">
<div class="form-group">
<label for="username">賬號(hào)</label>
<input type="text" class="form-control" name="username" value="" placeholder="賬號(hào)"/>
</div>
<div class="form-group">
<label for="password">密碼</label>
<input type="password" class="form-control" name="password" placeholder="密碼"/>
</div>
<input type="submit" id="login" value="Login" class="btn btn-primary"/>
</form>
</div>
</div>
</body>
</html>
ls的賬號(hào)去登錄成功會(huì)跳轉(zhuǎn)到hello頁(yè)面然而,請(qǐng)求admin頁(yè)面 則會(huì)報(bào)錯(cuò)no privilege
以下是參考的一些文章
http://blog.csdn.net/u012373815/article/details/54633046
http://blog.csdn.net/code__code/article/details/53885510
http://blog.csdn.net/u012367513/article/details/38866465
https://www.w3cschool.cn/springsecurity/uzq31ii7.html
源碼
https://github.com/sandnul025/springBoot-springSecurity.git