先來(lái)一波個(gè)人理解
Spring security權(quán)限驗(yàn)證流程圖
首先由WebSecurityConfig.class初始化用戶配置纷捞,可以配置登錄痢虹、登出、自定義的過(guò)濾器主儡、靜態(tài)資源等奖唯,然后由MyFilterSecurityIntercepyor.class攔截?cái)r截所有請(qǐng)求,然后分別進(jìn)行攔截驗(yàn)證和權(quán)限驗(yàn)證糜值,首先由MyInvocationSecurityMetadataSourceService.class判斷該請(qǐng)求是否需要攔截(是否需要權(quán)限驗(yàn)證)丰捷,如果不需要?jiǎng)t通過(guò),如果需要?jiǎng)t發(fā)送到MyAccessDecisionManager.class寂汇,由用戶自動(dòng)定義的CustomUserService.class獲取用戶信息病往,進(jìn)行權(quán)限驗(yàn)證,至此權(quán)限驗(yàn)證完畢骄瓣。
1. 搭建數(shù)據(jù)庫(kù)
/*Table structure for table `sys_permission` */
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
`descritpion` varchar(50) DEFAULT NULL,
`url` varchar(50) DEFAULT NULL,
`pid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `sys_permission` */
insert into `sys_permission`(`id`,`name`,`descritpion`,`url`,`pid`) values (1,'HOME','home','/',NULL),(2,'ADMIN','ABel','/admin',NULL);
/*Table structure for table `sys_permission_role` */
DROP TABLE IF EXISTS `sys_permission_role`;
CREATE TABLE `sys_permission_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) DEFAULT NULL,
`permission_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Data for the table `sys_permission_role` */
insert into `sys_permission_role`(`id`,`role_id`,`permission_id`) values (1,1,1),(2,1,2),(3,2,1);
/*Table structure for table `sys_role` */
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `sys_role` */
insert into `sys_role`(`id`,`name`) values (1,'ROLE_ADMIN'),(2,'ROLE_USER');
/*Table structure for table `sys_role_user` */
DROP TABLE IF EXISTS `sys_role_user`;
CREATE TABLE `sys_role_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sys_user_id` int(11) DEFAULT NULL,
`sys_role_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
/*Data for the table `sys_role_user` */
insert into `sys_role_user`(`id`,`sys_user_id`,`sys_role_id`) values (1,1,1),(2,2,2);
/*Table structure for table `sys_user` */
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Data for the table `sys_user` */
insert into `sys_user`(`id`,`username`,`password`) values (1,'admin','admin'),(2,'abel','abel');
2. 搭建項(xiàng)目停巷,引入pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.myspring.security</groupId>
<artifactId>mysecuritydemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>mysecuritydemo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
<!-- 在thymeleaf中擴(kuò)展spring secutity (頁(yè)面權(quán)限配置)-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
3. 逆向生成表信息,實(shí)現(xiàn)UserDetailsService.class
/**
* @author lht
* @doc 自定義用戶信息
* @date 2018/6/1
*/
@Service
public class CustomUserService implements UserDetailsService {
@Autowired
SysUserMapper sysUserMapper;
@Autowired
SysPermissionMapper sysPermissionMapper;
@Autowired
SysRoleMapper sysRoleMapper;
private Logger log= LoggerFactory.getLogger(this.getClass());
@Override
public UserDetails loadUserByUsername(String username) {
SysUser user = sysUserMapper.findByUserName(username);
if (user != null) {
List<SysPermission> permissions = sysPermissionMapper.findByAdminUserId(user.getId());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
List<SysRole> roles = sysRoleMapper.findRoleByUserid(user.getId());
for (SysRole role : roles) {
if (!StringUtils.isEmpty(role.getName())) {
log.info("role.getName():"+role.getName());
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getName());
//此處將角色信息添加到 GrantedAuthority 對(duì)象中榕栏,在后面進(jìn)行全權(quán)限驗(yàn)證時(shí)會(huì)使用GrantedAuthority 對(duì)象畔勤。
grantedAuthorities.add(grantedAuthority);
}
}
for (SysPermission permission : permissions) {
if (permission != null && permission.getName()!=null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
//1:此處將權(quán)限信息添加到 GrantedAuthority 對(duì)象中,在后面進(jìn)行全權(quán)限驗(yàn)證時(shí)會(huì)使用GrantedAuthority 對(duì)象臼膏。
grantedAuthorities.add(grantedAuthority);
}
}
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
} else {
throw new UsernameNotFoundException("admin: " + username + " do not exist!");
}
}
}
4. 實(shí)現(xiàn)AccessDecisionManager.class
/**
* @author lht
* @doc 權(quán)限判斷
* @date 2018/6/1
*/
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
// decide 方法是判定是否擁有權(quán)限的決策方法硼被,
//authentication 是釋CustomUserService中循環(huán)添加到 GrantedAuthority 對(duì)象中的權(quán)限信息集合.
//object 包含客戶端發(fā)起的請(qǐng)求的requset信息,可轉(zhuǎn)換為 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
//configAttributes 為MyInvocationSecurityMetadataSource的getAttributes(Object object)這個(gè)方法返回的結(jié)果渗磅,此方法是為了判定用戶請(qǐng)求的url 是否在權(quán)限表中嚷硫,如果在權(quán)限表中此疹,則返回給 decide 方法柑爸,用來(lái)判定用戶是否有此權(quán)限融痛。如果不在權(quán)限表中則放行谭羔。
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
return;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 為在注釋1 中循環(huán)添加到 GrantedAuthority 對(duì)象中的權(quán)限信息集合
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("no right");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
5. 實(shí)現(xiàn)FilterInvocationSecurityMetadataSource.class
/**
* @author lht
* @doc 設(shè)置攔截url權(quán)限和判斷權(quán)限
* @date 2018/6/1
*/
@Service
public class MyInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
private SysPermissionMapper sysPermissionMapper;
private HashMap<String, Collection<ConfigAttribute>> map =null;
/**
* 加載權(quán)限表中所有權(quán)限
*/
public void loadResourceDefine(){
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<SysPermission> permissions = sysPermissionMapper.findAll();
for(SysPermission permission : permissions) {
array = new ArrayList<>();
cfg = new SecurityConfig(permission.getName());
//此處只添加了用戶的名字栅组,其實(shí)還可以添加更多權(quán)限的信息榆俺,例如請(qǐng)求方法到ConfigAttribute的集合中去谒臼。此處添加的信息將會(huì)作為MyAccessDecisionManager類的decide的第三個(gè)參數(shù)艇挨。
array.add(cfg);
//用權(quán)限的getUrl() 作為map的key会烙,用ConfigAttribute的集合作為 value负懦,
map.put(permission.getUrl(), array);
}
}
//此方法是為了判定用戶請(qǐng)求的url 是否在權(quán)限表中筒捺,如果在權(quán)限表中,則返回給 decide 方法纸厉,用來(lái)判定用戶是否有此權(quán)限系吭。如果不在權(quán)限表中則放行。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) loadResourceDefine();
//object 中包含用戶請(qǐng)求的request 信息
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
String resUrl;
for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
resUrl = iter.next();
matcher = new AntPathRequestMatcher(resUrl);
if(matcher.matches(request)) {
return map.get(resUrl);
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
6. 繼承AbstractSecurityInterceptor.class同時(shí)實(shí)現(xiàn)Filter.class
/**
* @author lht
* @doc 攔截器(所有權(quán)限驗(yàn)證的調(diào)用轉(zhuǎn)發(fā)地)
* @date 2018/6/1
*/
@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 {
//fi里面有一個(gè)被攔截的url
//里面調(diào)用MyInvocationSecurityMetadataSource的getAttributes(Object object)這個(gè)方法獲取fi對(duì)應(yīng)的所有權(quán)限
//再調(diào)用MyAccessDecisionManager的decide方法來(lái)校驗(yàn)用戶的權(quán)限是否足夠
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;
}
}
7. 繼承WebSecurityConfigurerAdapter.class
/**
* @author lht
* @doc security 規(guī)則配置
* @date 2018/6/1
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//開(kāi)啟security的注解模式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* @author lht
* @doc 加載自定義獲得用戶信息(根據(jù)username)
* @date 2018/6/1
*/
@Bean
UserDetailsService customUserService(){ //注冊(cè)UserDetailsService 的bean
return new CustomUserService();
}
/**
* @author lht
* @doc 設(shè)置密碼為明文(在實(shí)際項(xiàng)目中不行)
* @date 2018/6/1
*/
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()); //user Details Service驗(yàn)證
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //任何請(qǐng)求,登錄后可以訪問(wèn)
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.permitAll() //登錄頁(yè)面用戶任意訪問(wèn)
.and()
.logout().permitAll(); //注銷(xiāo)行為任意訪問(wèn)
// 范例
/* http.authorizeRequests()
//靜態(tài)資源和一些所有人都能訪問(wèn)的請(qǐng)求
.antMatchers("/css/**","/staic/**", "/js/**","/images/**").permitAll()
.antMatchers("/", "/login","/session_expired").permitAll()
//登錄
.and().formLogin()
.loginPage("/login")
.usernameParameter("userId") //自己要使用的用戶名字段
.passwordParameter("password") //密碼字段
.defaultSuccessUrl("/index") //登陸成功后跳轉(zhuǎn)的請(qǐng)求,要自己寫(xiě)一個(gè)controller轉(zhuǎn)發(fā)
.failureUrl("/loginAuthtictionFailed") //驗(yàn)證失敗后跳轉(zhuǎn)的url
//session管理
.and().sessionManagement()
.maximumSessions(1) //系統(tǒng)中同一個(gè)賬號(hào)的登陸數(shù)量限制
.and().and()
.logout()//登出
.invalidateHttpSession(true) //使session失效
.clearAuthentication(true) //清除證信息
.and()
.httpBasic();*/
}
}
8. 實(shí)現(xiàn)登錄頁(yè)面(注:在開(kāi)始沒(méi)有更改默認(rèn)配置的情況下颗品,用戶名和密碼只能是username和password)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<input type="text" id="username" name="username"/>
<input type="password" id="password" name="password"/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
9. 實(shí)現(xiàn)頁(yè)面權(quán)限
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta content="text/html;charset=UTF-8"/>
<link rel="stylesheet" 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>
<li><a th:href="@{/admin}"> admin </a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<div class="starter-template">
<h1 th:text="${msg.title}"></h1>
<p class="bg-primary" th:text="${msg.content}"></p>
<div > <!-- 用戶類型為ROLE_ADMIN 顯示 -->
<p class="bg-info" th:text="${msg.etraInfo}"></p>
</div>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="注銷(xiāo)"/>
</form>
</div>
</div>
</body>
</html>
最后附上源碼連接:https://github.com/lhtGit/Spring-security-demo
結(jié)束語(yǔ)
至此Spring security就算是搭建完成肯尺,實(shí)現(xiàn)了基本權(quán)限的設(shè)置。但是還有一些問(wèn)題并沒(méi)與解決和實(shí)現(xiàn):
- 權(quán)限和角色沒(méi)有分開(kāi)躯枢,混合在了一起则吟,所以說(shuō)并沒(méi)有真正的實(shí)現(xiàn)角色和權(quán)限的管理
- 沒(méi)有實(shí)現(xiàn)驗(yàn)證碼部分
- 。锄蹂。氓仲。剛剛搭建,還沒(méi)有過(guò)多的感觸
2018-6-14 更新——加入驗(yàn)證碼
public class VerifyFilter extends OncePerRequestFilter {
private static final PathMatcher pathMatcher = new AntPathMatcher();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(isProtectedUrl(request)&&!validateVerify(request.getSession())){
request.getRequestDispatcher("/login?Verify").forward(request,response);
}else{
filterChain.doFilter(request,response);
}
}
//判斷驗(yàn)證碼是否已經(jīng)通過(guò)
private boolean validateVerify(HttpSession session){
return StringUtils.isEmpty(session.getAttribute(Common.verify))?false :(boolean) session.getAttribute(Common.verify);
}
// 攔截 /login的POST請(qǐng)求
private boolean isProtectedUrl(HttpServletRequest request) {
return "POST".equals(request.getMethod()) && pathMatcher.match("/login", request.getServletPath());
}
}
需要注意的是需要繼承OncePerRequestFilter 败匹,該類是所有security的filter的父類寨昙,
最后記得要在WebSecurityConfig的configure方法中加入這段話,才會(huì)起作用:
http.addFilterBefore(new VerifyFilter(),UsernamePasswordAuthenticationFilter.class);
該驗(yàn)證使用的是極限驗(yàn)證掀亩,不過(guò)是自己寫(xiě)的舔哪,整個(gè)驗(yàn)證的大概思路:
- 首先判斷是否已經(jīng)驗(yàn)證通過(guò)了(這個(gè)是極限驗(yàn)證部分的),如果通過(guò)了就在session中放一個(gè)布爾值槽棍,用以標(biāo)記
- 在VerifyFilter 過(guò)濾器中取出該標(biāo)記(一定要記得它在什么位置捉蚤,例如在login登錄頁(yè),只是在登錄時(shí)判斷炼七,那么就要判斷當(dāng)前的url是否是login缆巧,什么方式的(get/post),好讓程序能夠正確執(zhí)行)