本案例通过mybatis为持久层,自定义了用户和配套权限,在请求Spring boot web的controller方法时做不同权限的控制。代码在https://github.com/yejingtao/forblog/tree/master/mydemo-security。
用户权限初始化脚本:
--create schema
create schema schema3;
--create sysuser table
--password length shoud be larger than 60
create table schema3.sysuser(
id int(10) not null AUTO_INCREMENT,
username varchar(10) not null,
password varchar(100) not null,
primary key (id)
);
--create sysrole table
create table schema3.sysauthority(
id int(10) not null AUTO_INCREMENT,
name varchar(20) not null,
description varchar(20),
primary key (id)
);
--create bridge table
create table schema3.r_user_authority(
user_id int(10) not null,
authority_id int(10) not null
);
--init dml
insert into schema3.sysauthority(name,description) values("authority_a","for role a test");
insert into schema3.sysauthority(name,description) values("authority_b","for role b test");
--usera password usera123
--userb password userb123
--userab password userab123
--userc password userc123
insert into schema3.sysuser(username,password) values("usera","$2a$10$DbCNZwHe/OwynRDMsNU7qO9QkpL0c5ZxWypoBf7QAOBXFCjkC6LZC");
insert into schema3.sysuser(username,password) values("userb","$2a$10$ab0wFJHZx.x4dp..ZaeC8ePt2cSmYVXbNBFDVZnTD133xx.Qsb05G");
insert into schema3.sysuser(username,password) values("userab","$2a$10$DdKeZYGBi1msdaXKDjkreOO2eIczINK8.QkQubpSStukFZRHumvTK");
insert into schema3.sysuser(username,password) values("userc","$2a$10$40JDO3oL9uSi68r4GgRSae7/f2Arq2KT3zQ2ZzyNhA6bUKrodOyDS");
insert into schema3.r_user_authority(user_id,authority_id) values((select id from schema3.sysuser where username="usera"),(select id from schema3.sysauthority where name="authority_a"));
insert into schema3.r_user_authority(user_id,authority_id) values((select id from schema3.sysuser where username="userb"),(select id from schema3.sysauthority where name="authority_b"));
insert into schema3.r_user_authority(user_id,authority_id) values((select id from schema3.sysuser where username="userab"),(select id from schema3.sysauthority where name="authority_a"));
insert into schema3.r_user_authority(user_id,authority_id) values((select id from schema3.sysuser where username="userab"),(select id from schema3.sysauthority where name="authority_b"));
先看Pom依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
起权限作用的包是spring-boot-starter-security。
再看application.properties配置,没有特别之处。
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://192.168.226.130:3306/schema3
username: root
password: admin123
driver-class-name: com.mysql.jdbc.Driver
jpa:
database: MYSQL
show-sql: true
application:
name: mydemo
logging:
file: ./spring-boot.log
level:
org.springframework.web: DEBUG
com.mydemo.dao: DEBUG
log级别的调整是为了打印mybatis的sql语句方便调试。
再看主程序Application,内容比较多,我们一一详解。
package com.mydemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.mydemo.service.MybatisUserDetailsService;
@SpringBootApplication
@MapperScan("com.mydemo.dao")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Configuration
@EnableWebSecurity
protected static class webSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MybatisUserDetailsService userDetailsService;// 这个名字没得挑,userDetailsService是自定义鉴权的名字。
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.authorizeRequests().antMatchers("/", "/hello").permitAll()
// .antMatchers("/forRoleA/**").access("hasAuthority('authority_a')")
// .antMatchers("/forRoleB/**").access("hasAuthority('authority_b')")
// .antMatchers("/forRoleA/**").hasAuthority("authority_a")
// .antMatchers("/forRoleB/**").hasAuthority("authority_b")
.and().formLogin();
/**
* .loginPage("/login") .failureUrl("/login?error") .permitAll() //登录页面用户任意访问
*
* .and() .logout().permitAll(); //注销行为任意访问
*/
}
// 验证时给密码加密,因为数据库里的也是BCryptPasswordEncoder加密的
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
}
@MapperScan("com.mydemo.dao")为了指定mybatis的mapping配置的位置;@EnableGlobalMethodSecurity(prePostEnabled= true)不是必须的,先忽略,后面再细讲。
启动web权限控制需要@EnableWebSecurity注解,具体配置需要继承WebSecurityConfigurerAdapter。其中userDetailsService是自定义用户和权限控制的服务,必须实现UserDetailsService,被DaoAuthenticationProvider调用。
configure(AuthenticationManagerBuilder auth)定义了用户密码的验证和密码的加密方式,这里我采用的是目前最安全的BCrypt加密方式。最开始流行的是MD5加密和哈希,安全性实在太差,容易被人查表法破解(将一些比较常用的密码的哈希值算好,然后建立一张表,根据数据库里hash值反向查密码),后来升级为Hash+Salt加密(用户的密码就分为了盐和密文两部分,其中盐是注册时随机生成的,越长越好),安全级别提高不少,但是同样存在数据库中密文和盐一起泄露的可能性,攻击者拿到密文和盐暴力破解也是几天的事。Bcrypt加密就是把salt随机混入了最终的加密,不再单独保存salt,通过增加计算密文的成本来换取安全。使用Bcrypt,默认设置下登陆时间会增加5个数量级,破解成本也会增加5个数量级。这里注意数据存的密码列要足够大,至少要varchar60才能存的下Bcrypt密码。Test的PasswordGet提供了加密的密码。
对具体的controller方法进行权限的控制有2种方式,代码示例中都已提供。
方式一:静态配置到configure(HttpSecurity http)中,其中注释的两种方法是等效的
// .antMatchers("/forRoleA/**").access("hasAuthority('authority_a')")
// .antMatchers("/forRoleB/**").access("hasAuthority('authority_b')")
// .antMatchers("/forRoleA/**").hasAuthority("authority_a")
// .antMatchers("/forRoleB/**").hasAuthority("authority_b")
方式二:通过@PreAuthorize配置,详见controller代码,请注意如果要用注释法Application中要添加@EnableGlobalMethodSecurity(prePostEnabled = true)
.and().formLogin()表示权限不足时自动跳转到登录页面,如果后面增加.loginPage("/login")标识自定义了登录页面,根据是jsp还是模板去对应目录里去找(http://blog.csdn.net/yejingtao703/article/details/77414711),如果没有.logPage()Spring自己提供了默认的登陆页面
MybatisUserDetailsService中核心方法是接口UserDetailsService中的loadUserByUsername方法,代码比较容易读懂,给登录用户组装相应的权限。
package com.mydemo.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.Service;
import com.mydemo.dao.AuthorityRepository;
import com.mydemo.dao.UserRepository;
import com.mydemo.entity.SysAuthority;
import com.mydemo.entity.SysUser;
@Service
public class MybatisUserDetailsService implements UserDetailsService{
@Autowired
private AuthorityRepository authorityRepository;
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//先判断登陆用户是否存在
SysUser user = userRepository.findByUserName(userName);
if(user==null) {
throw new UsernameNotFoundException("Cannot find user by username: "+userName);
}
//登录用户存在的情况加载用户的权限
/**
SecurityUser securityUser = new SecurityUser(user);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return securityUser;
*/
List<SysAuthority> roleList = authorityRepository.findByUserId(user.getId());
List<GrantedAuthority> grantedAuthorities = new ArrayList <GrantedAuthority>();
if(roleList!=null && roleList.size()>0) {
roleList.forEach(r->grantedAuthorities.add(new SimpleGrantedAuthority(r.getName())));
}
return new User(user.getUserName(),user.getPassword(),grantedAuthorities);
}
}
测试起来很简单,hello可以随意进入,roleTestMothedA只有带有A权限的才可以进入,roleTestMothedB只有带有B权限的才可以进入,我们准备了4个用户供测试,usera、userb、userab、userc都试着操作下。
如果直接访问/hello:
如果直接访问/forRoleA或者/forRoleB,会跳转到登陆页面:
输入密码密码:
输入正确后:
输入错误后: