先来一波个人理解
首先由WebSecurityConfig.class初始化用户配置,可以配置登录、登出、自定义的过滤器、静态资源等,然后由MyFilterSecurityIntercepyor.class拦截拦截所有请求,然后分别进行拦截验证和权限验证,首先由MyInvocationSecurityMetadataSourceService.class判断该请求是否需要拦截(是否需要权限验证),如果不需要则通过,如果需要则发送到MyAccessDecisionManager.class,由用户自动定义的CustomUserService.class获取用户信息,进行权限验证,至此权限验证完毕。
1. 搭建数据库
/*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. 搭建项目,引入pom文件
4.0.0
com.myspring.security
mysecuritydemo
0.0.1-SNAPSHOT
jar
mysecuritydemo
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
2.0.2.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.0
org.thymeleaf.extras
thymeleaf-extras-springsecurity4
org.springframework.boot
spring-boot-maven-plugin
src/main/java
**/*.properties
**/*.xml
false
3. 逆向生成表信息,实现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 permissions = sysPermissionMapper.findByAdminUserId(user.getId());
List grantedAuthorities = new ArrayList<>();
List 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 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
grantedAuthorities.add(grantedAuthority);
}
}
for (SysPermission permission : permissions) {
if (permission != null && permission.getName()!=null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
//1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
grantedAuthorities.add(grantedAuthority);
}
}
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
} else {
throw new UsernameNotFoundException("admin: " + username + " do not exist!");
}
}
}
4. 实现AccessDecisionManager.class
/**
* @author lht
* @doc 权限判断
* @date 2018/6/1
*/
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
// decide 方法是判定是否拥有权限的决策方法,
//authentication 是释CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
//object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
//configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
return;
}
ConfigAttribute c;
String needRole;
for(Iterator iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
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. 实现FilterInvocationSecurityMetadataSource.class
/**
* @author lht
* @doc 设置拦截url权限和判断权限
* @date 2018/6/1
*/
@Service
public class MyInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
private SysPermissionMapper sysPermissionMapper;
private HashMap> map =null;
/**
* 加载权限表中所有权限
*/
public void loadResourceDefine(){
map = new HashMap<>();
Collection array;
ConfigAttribute cfg;
List permissions = sysPermissionMapper.findAll();
for(SysPermission permission : permissions) {
array = new ArrayList<>();
cfg = new SecurityConfig(permission.getName());
//此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
array.add(cfg);
//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
map.put(permission.getUrl(), array);
}
}
//此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
@Override
public Collection getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) loadResourceDefine();
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
String resUrl;
for(Iterator 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 getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
}
6. 继承AbstractSecurityInterceptor.class同时实现Filter.class
/**
* @author lht
* @doc 拦截器(所有权限验证的调用转发地)
* @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里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
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 规则配置
* @date 2018/6/1
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启security的注解模式
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* @author lht
* @doc 加载自定义获得用户信息(根据username)
* @date 2018/6/1
*/
@Bean
UserDetailsService customUserService(){ //注册UserDetailsService 的bean
return new CustomUserService();
}
/**
* @author lht
* @doc 设置密码为明文(在实际项目中不行)
* @date 2018/6/1
*/
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()); //user Details Service验证
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //任何请求,登录后可以访问
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.permitAll() //登录页面用户任意访问
.and()
.logout().permitAll(); //注销行为任意访问
// 范例
/* http.authorizeRequests()
//静态资源和一些所有人都能访问的请求
.antMatchers("/css/**","/staic/**", "/js/**","/images/**").permitAll()
.antMatchers("/", "/login","/session_expired").permitAll()
//登录
.and().formLogin()
.loginPage("/login")
.usernameParameter("userId") //自己要使用的用户名字段
.passwordParameter("password") //密码字段
.defaultSuccessUrl("/index") //登陆成功后跳转的请求,要自己写一个controller转发
.failureUrl("/loginAuthtictionFailed") //验证失败后跳转的url
//session管理
.and().sessionManagement()
.maximumSessions(1) //系统中同一个账号的登陆数量限制
.and().and()
.logout()//登出
.invalidateHttpSession(true) //使session失效
.clearAuthentication(true) //清除证信息
.and()
.httpBasic();*/
}
}
8. 实现登录页面(注:在开始没有更改默认配置的情况下,用户名和密码只能是username和password)
Title
9. 实现页面权限
最后附上源码连接:https://github.com/lhtGit/Spring-security-demo
结束语
至此Spring security就算是搭建完成,实现了基本权限的设置。但是还有一些问题并没与解决和实现:
- 权限和角色没有分开,混合在了一起,所以说并没有真正的实现角色和权限的管理
- 没有实现验证码部分
- 。。。刚刚搭建,还没有过多的感触
2018-6-14 更新——加入验证码
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);
}
}
//判断验证码是否已经通过
private boolean validateVerify(HttpSession session){
return StringUtils.isEmpty(session.getAttribute(Common.verify))?false :(boolean) session.getAttribute(Common.verify);
}
// 拦截 /login的POST请求
private boolean isProtectedUrl(HttpServletRequest request) {
return "POST".equals(request.getMethod()) && pathMatcher.match("/login", request.getServletPath());
}
}
需要注意的是需要继承OncePerRequestFilter ,该类是所有security的filter的父类,
最后记得要在WebSecurityConfig的configure方法中加入这段话,才会起作用:
http.addFilterBefore(new VerifyFilter(),UsernamePasswordAuthenticationFilter.class);
该验证使用的是极限验证,不过是自己写的,整个验证的大概思路:
- 首先判断是否已经验证通过了(这个是极限验证部分的),如果通过了就在session中放一个布尔值,用以标记
- 在VerifyFilter 过滤器中取出该标记(一定要记得它在什么位置,例如在login登录页,只是在登录时判断,那么就要判断当前的url是否是login,什么方式的(get/post),好让程序能够正确执行)