SpringSecurity简单入门 SpringSecurity 是一个灵活和强大的身份验证和访问控制的安全框架,它确保基于Spring的应用程序提供身份验证和授权支持。它与Spring MVC有很好地集成,并配备了流行的安全算法实现捆绑在一起。
环境:SpringBoot 2.1 + Mybatis + SpringSecurity 5.0
Spring Security 模块
核心模块 - spring-security-core.jar:包含核心验证和访问控制类和接口,远程支持的基本配置API,是基本模块。
远程调用 - spring-security-remoting.jar:提供与 Spring Remoting 集成。
网页 - spring-security-web.jar:包括网站安全的模块,提供网站认证服务和基于URL访问控制。
配置 - spring-security-config.jar:包含安全命令空间解析代码,若使用XML进行配置则需要。
LDAP - spring-security-ldap.jar:LDAP 验证和配置,若需要LDAP验证和管理LDAP用户实体。
ACL访问控制表 - spring-security-acl.jar:ACL专门领域对象的实现。
CAS - spring-security-cas.jar:CAS客户端继承,若想用CAS的SSO服务器网页验证。
OpenID - spring-security-openid.jar:OpenID网页验证支持。
Test - spring-security-test.jar:支持Spring Security的测试。
认证流程
用户使用用户名和密码登录。
用户名密码被过滤器(默认为UsernamePasswordAuthenticationFilter)获取到,封装成 Authentication。
token(Authentication实现类)传递给 AuthenticationManager 进行认证。
AuthenticationManager 认证成功后返回一个封装了用户权限信息的 Authentication 对象。
通过调用 SecurityContextHolder.getContext().setAuthentication(…) 将 Authentication 对象赋给当前的 SecurityContext。
启动原理 启动demo 引入相关依赖后,根据Spring Security官方文档给出的例子创建类。
1 2 3 4 5 6 7 8 9 @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); } }
以上我们便将spring security应用到我们的项目中了,上面的例子,在内存中配置了一个用户名为user,密码为password,并且拥有USER角色的用户。想要知道它是怎么运行的,请往下看。
WebSecurityConfigurer的子类可以扩展spring security的应用, 而WebSecurityConfigurerAdapter是WebSecurityConfigurer 的一个适配器,必然也是做了很多默认的工作。
入手 @EnableWebSecurity注解 跟进@EnableWebSecurity看下源码:
1 2 3 4 5 6 7 8 9 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class}) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { boolean debug() default false; }
Tips:
@EnableWebSecurity 配置到拥有注解 @Configuration 的类上,就可以获取到spring security的支持.
跟进 SpringWebMvcImportSelector SpringWebMvcImportSelector 的作用是判断当前的环境是否包含springmvc,因为spring security可以在非spring环境下使用,为了避免DispatcherServlet的重复配置,所以使用了这个注解来区分。
跟进 @EnableGlobalAuthentication 注解的源码如下:
1 2 3 4 @Import(AuthenticationConfiguration.class) @Configuration public @interface EnableGlobalAuthentication { }
可以看出,这个注解引入了AuthenticationConfiguration配置。而这个类用来配置认证相关,主要任务就是生成全局的身份认证管理者。AuthenticationManager:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration @Import(ObjectPostProcessorConfiguration.class) public class AuthenticationConfiguration { private AuthenticationManager authenticationManager; @Bean public AuthenticationManagerBuilder authenticationManagerBuilder( ObjectPostProcessor<Object> objectPostProcessor) { return new AuthenticationManagerBuilder(objectPostProcessor); } public AuthenticationManager getAuthenticationManager() throws Exception { ... } }
跟进 WebSecurityConfiguration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Autowired(required = false) public void setFilterChainProxySecurityConfigurer( ObjectPostProcessor<Object> objectPostProcessor, //T1 使用@Value获取到配置信息 @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getebSecurityConfigurers()}") List<SecurityConfigurer<Filter,WebSecurity>> webSecurityConfigurers) throws Exception { //T2 创建一个webSecurity 对象 webSecurity = objectPostProcessor .postProcess(new WebSecurity(objectPostProcessor)); if (debugEnabled != null) { webSecurity.debug(debugEnabled); } //T3对configures进行排序 Collections.sort(webSecurityConfigurers,AnnotationAwareOrderComparator.INSTANCE); //T4对Order进行比较是否有相同的,由于前面进行了排序,只要比较后有相同的就可以 Integer previousOrder = null; Object previousConfig = null; for (SecurityConfigurer<Filter, WebSecurity> config :webSecurityConfigurers) { Integer order =AnnotationAwareOrderComparator.lookupOrder(config); if (previousOrder != null && previousOrder.equals(order)) { throw new IllegalStateException( "@Order on WebSecurityConfigurers must beunique. Order of " + order + " was already used on " +previousConfig + ", so it cannot be usedon " + config + " too."); } previousOrder = order; previousConfig = config; } for (SecurityConfigurer<Filter, WebSecurity>webSecurityConfigurer : webSecurityConfigurers) { //T5将配置信息配置到webSecurity中 webSecurity.apply(webSecurityConfigurer); } this.webSecurityConfigurers = webSecurityConfigurers; }
上述代码中T1标记处,我们看一下autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()的源代码
1 2 3 4 5 6 7 8 9 10 private final ConfigurableListableBeanFactory beanFactory; public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() { List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<SecurityConfigurer<Filter, WebSecurity>>(); Map<String, WebSecurityConfigurer> beansOfType = beanFactory .getBeansOfType(WebSecurityConfigurer.class); for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) { webSecurityConfigurers.add(entry.getValue()); } return webSecurityConfigurers; }
这个beansOfType 就是我们定义的继承自WebSecurityConfigurerAdapter的类, 通过查看父类的定义,我们知道调用build()方法最后返回的必须是一个Filter对象,可以自行参考顶级父类(或接口)WebSecurityConfigurer和SecurityBuilder
springSecurityFilterChain() 为我们创建了一个名字叫做springSecurityFilterChain的Filter 源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /** * Creates the Spring Security Filter Chain * @return the {@link Filter} that represents the security filterchain * @throws Exception */ @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTR_NAME) public Filter springSecurityFilterChain() throws Exception { //T1 查看是否有WebSecurityConfigurer的相关配置 boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); //T2 如果没有,说明我们没有注入继承WebSecurityConfigurerAdapter的对象(没有创建其子类) if (!hasConfigurers) { //T3 创建默认的配置信息WebSecurityConfigurerAdapter,保证SpringSecurity的 //最基础的功能,如果我们要有自定义的相关,一定要重配置 WebSecurityConfigurerAdapter adapter =objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter() { }); //T4 默认配置信息载入webSecurity webSecurity.apply(adapter); } // T5这里build一个Filter return webSecurity.build(); }
webSecurity对象在此时已经加载完所有的配置。
webSecurity对象为我们创建一个Filter通过的是build()方法。
注意: 建立了一个Filter对象,而这个Filter将会拦截掉我们的请求,对请求进行过滤拦截,从而起到对资源进行认证保护的作用。然后这个Filter并非我们自己平时定义的Filter这么简单,这个过滤器也只是一个代理的过滤器而已,里面还会有过滤器链。
WebSecurity的build()方法 WebSecurity继承了AbstractConfiguredSecurityBuilder类,实现了SecurityBuilder接口。
事实上AbstractConfiguredSecurityBuilder类的父类AbstractSecurityBuilder也是实现了SecurityBuilder接口。子类和父类实现同一个接口。事实上是为了子类在反射调用方法getInterfaces()中可以获取到接口,根据这里的情况就是WebSecurity反射调用getInterfaces()可以获取到SecurityBuilder接口。
WebSecurity 类图
SecurityBuilder定义了构建的接口标准
AbstractSecurityBuilder实现build方法,用AtomicBoolean的变量building保证多线程情况下,操作的原子性。此处采用的是模板模式。定义了doBuild()抽象方法,用来给子类实现。
AbstractConfiguredSecurityBuilder 继承AbstractSecurityBuilder实现doBuild()方法,也采用模板模式,定义了实现的具体的步骤,如UNBUILT,INITIALIZING,CONFIGURING,BUILDING,以及BUILT。
我们看一下WebSecurity的定义
1 2 3 4 5 6 public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements SecurityBuilder<Filter>, ApplicationContextAware { ... ... }
从上面的代码可以看出WebSecurity指定泛型的类型为Filter,结合上面接口build()方法我们可以知道,WebSecurity的build()方法返回的是一个Filter,Spring Securiy 通过这个来创建一个过滤器。
build()过程
AbstractSecurityBuilder保证了线程的安全。
AbstractConfiguredSecurityBuilder保证了构建过程以及构建状态。
WebSecurity通过performBuild()来实现自身的构建
AbstractConfiguredSecurityBuilder的构建过程,我们看一下doBuild()方法的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; //默认什么都没做,WebSecurity也没有重写 beforeInit(); //T1默认此处调用WebSecurityConfigurerAdapter的init(finalWebSecurity web)方法 init(); buildState = BuildState.CONFIGURING; //默认什么都不做,WebSecurity没有重写 beforeConfigure(); //调用WebSecurityConfigurerAdapter的configure(WbSecurity web),但是什么都没做 configure(); buildState = BuildState.BUILDING; //T2这里调用WebSecurity的performBuild()方法 O result = performBuild(); buildState = BuildState.BUILT; //从WebSecurity的实现,这里返回了一个Filter,完成构建过程 return result; } }
T1 处调用WebSecurityConfigurerAdapter的init(final WebSecurity web)方法,看一下源代码的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * @param web * @throws Exception */ public void init(final WebSecurity web) throws Exception { //构建HttpSecurity对象 final HttpSecurity http = getHttp(); //Ta web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() { public void run() { FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); } }); }
这里构建了HttpSecurity对象,以及有一个共享对象FilterSecurityInterceptor。
Ta 处调用了WebSecurity的addSecurityFilterChainBuilder()方法,我们看一下这个方法的源代码1 2 3 4 5 6 public WebSecurity addSecurityFilterChainBuilder( SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) { //这个就是securityFilterChainBuilders变量 this.securityFilterChainBuilders.add(securityFilterChainBuilder); return this; }
在构建Filter过程的初始化的时候,我们对securityFilterChainBuilders这个变量进行了赋值,默认情况下securityFilterChainBuilders里面只有一个对象,那就是HttpSecurity。
T2 根据WebSecurity的属性构建Filter的performBuild()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Override protected Filter performBuild() throws Exception { Assert.state( //T1 securityFilterChainBuilders哪里来的? !securityFilterChainBuilders.isEmpty(), () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. " + "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. " + "More advanced users can invoke " + WebSecurity.class.getSimpleName() + ".addSecurityFilterChainBuilder directly"); //T2 ignoredRequests.size()到底是什么? int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); //这个securityFilterChains 的集合里面存放的就是我们所有的过滤器链,根据长度的定义, //我们也可以知道分为两种一个是通过 ignoredRequests 来的过滤器链, //一个是通过 securityFilterChainBuilders 这个过滤器链构建链来的。 List<SecurityFilterChain> securityFilterChains = new ArrayList<>( chainSize); //如果是 ignoredRequest类型的,那么就添加默认过滤器链(DefaultSecurityFilterChain) for (RequestMatcher ignoredRequest : ignoredRequests) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); } //如果是securityFilterChainBuilder类型的,那么通过securityFilterChainBuilder的build()方法来构建过滤器链 for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); } //将过滤器链交给一个过滤器链代理对象,而这个代理对象就是返回回去的 //过滤器。到这里为止,过滤器的过程已经结束 //T3 什么是FilterChainProxy? FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); if (httpFirewall != null) { filterChainProxy.setFirewall(httpFirewall); } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; if (debugEnabled) { logger.warn("\n\n" + "********************************************************************\n" + "********** Security debugging is enabled. *************\n" + "********** This may include sensitive information. *************\n" + "********** Do not use in a production system! *************\n" + "********************************************************************\n\n"); result = new DebugFilter(filterChainProxy); } postBuildAction.run(); return result; }
WebSecurity的securityFilterChainBuilders属性哪里来的? 见上边解释
ignoredRequest是什么? ignoredRequests只是WebSecurity的一个属性
ignoredRequests的list中的值从哪里来的呢? 我们可以看到里面有一个ignore()方法,通过这个来进行设置的 怎么设置呢,那我们得看看WebSecurityConfigurerAdapter这个类了,里面有一个configure方法供我们重写
1 2 3 4 5 6 7 8 9 public void configure(WebSecurity web) throws Exception { } #具体的例子如下 @Override public void configure(WebSecurity web) throws Exception { super.configure(web); web.ignoring() .mvcMatchers("/favicon.ico", "/webjars/**", "/css/**"); }
然后值得一提的是这里有多少个mvcMatchers就会创建多少个ignoredRequests的对象,也就会有多少个过滤器链,也是在WebSecurity里面定义的内部类IgnoredRequestConfigurer这个类里面。
FilterChainProxy到底是什么 上面的描述中我们知道, FilterChainProxy是真正返回的Filter,上面代码中 FilterChainProxy的对象创建的源码为:
1 FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
Filter实现的流程
小结 SpringSecurityFilterChain 是Spring Security认证的入口。集成Spring Boot集成之后,xml配置被java注解配置取代,也就是 在WebSecurityConfiguration中完成了声明springSecurityFilterChain的作用。 并且最终交给DelegatingFilterProxy这个代理类,负责拦截请求。
也就是说:**@EnableWebSecurity完成的工作便是加载了WebSecurityConfiguration,AuthenticationConfiguration这两个核心配置类,也就此将spring security的职责划分为了配置安全信息,配置认证信息两部分。**
HttpSecurity 通过getHttp()获取,后面会详细说到这个类
UserDetailsService 用户信息获取
AuthenticationManager 认证管理类
SecurityContextHolder
SecurityContextHolder 用于存储安全上下文信息(如操作用户是谁、用户是否被认证、用户权限有哪些),它用 ThreadLocal 来保存 SecurityContext,者意味着 Spring Security 在用户登录时自动绑定到当前现场,用户退出时,自动清除当前线程认证信息,SecurityContext 中含有正在访问系统用户的详细信息
导入依赖 导入 spring-boot-starter-security 依赖,在 SpringBoot 2.1 环境下默认使用的是 5.0 版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <parent > <groupId > org.springframework.boot</grupId > <artifactId > spring-boot-starter-paren</artifactId > <version > 2.1.3.RELEASE</version > <relativePath /> </parent > <groupId > com.jelly</groupId > <artifactId > spring-boot-security</artifacId > <version > 0.0.1-SNAPSHOT</version > <name > spring-boot-security</name > <description > Demo project for SpringBoot</description > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.springframework.boot/groupId> <artifactId > spring-boot-starter-scurity</artifactId > </dependency > <dependency > <groupId > org.springframework.boot/groupId> <artifactId > spring-boot-starter-wb</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java<artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.mybatis.spring.boot<groupId > <artifactId > mybatis-spring-boot-sarter</artifactId > <version > 1.2.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupd > <artifactId > lombok</artifactId > <version > 1.18.6</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.springframework.boot/groupId> <artifactId > spring-boot-starter-tst</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.secuity</groupId > <artifactId > spring-security-test<artifactId > <scope > test</scope > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.oot</groupId > <artifactId > spring-boot-mavenplugin</artifactId > </plugin > </plugins > </build >
创建数据库 一般权限控制有三层,即:用户<–>角色<–>权限,用户与角色是多对多,角色和权限也是多对多。这里我们先暂时不考虑权限,只考虑用户<–>角色。
创建用户表sys_user:
1 2 3 4 5 6 CREATE TABLE `sys_user` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `name` varchar (255 ) NOT NULL , `password` varchar (255 ) NOT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
创建权限表sys_role:
1 2 3 4 5 CREATE TABLE `sys_role` ( `id` int (11 ) NOT NULL , `name` varchar (255 ) NOT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
创建用户-角色表sys_user_role:
1 2 3 4 5 6 7 8 CREATE TABLE `sys_user_role` ( `user_id` int(11) NOT NULL, `role_id` int(11) NOT NULL, PRIMARY KEY (`user_id`,`role_id`), KEY `fk_role_id` (`role_id`), CONSTRAINT `fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
初始化一下数据:
1 2 3 4 5 6 INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN'); INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER'); INSERT INTO `sys_user` VALUES ('1', 'admin', '123'); INSERT INTO `sys_user` VALUES ('2', 'jitwxs', '123'); INSERT INTO `sys_user_role` VALUES ('1', '1'); INSERT INTO `sys_user_role` VALUES ('2', '2');
注意: 这里的权限格式为 ROLE_XXX ,是Spring Security规定的,不要乱起名字哦。
准备页面 因为是示例程序,页面越简单越好,只用于登陆的login.html以及用于登陆成功后的home.html,将其放置在 resources/static 目录下:
login.html: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登陆</title> </head> <body> <h1>登陆</h1> <form method="post" action="/login"> <div> 用户名:<input type="text" name="username"> </div> <div> 密码:<input type="password" name="password"> </div> <div> <button type="submit">立即登陆</button> </div> </form> </body> </html>
注意: 用户的登陆认证是由Spring Security进行处理的,请求路径默认为/login,用户名字段默认为username,密码字段默认为password
home.html 1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>登陆成功</h1> <a href="/admin">检测ROLE_ADMIN角色</a> <a href="/user">检测ROLE_USER角色</a> <button onclick="window.location.href='/logout'">退出登录</button> </body> </html>
配置application.properties 在配置文件中配置下数据库连接:
数据源 1 2 3 4 5 6 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&useSSL=true spring.datasource.username=root spring.datasource.password=123456 #开启Mybatis下划线命名转驼峰命名 mybatis.configuration.map-underscore-to-camel-case=true
创建实体、Dao、Service和Controller 实体 SysUser 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.jelly.security.bean; import lombok.Data; import java.io.Serializable; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 18:53 **/ @Data public class SysUser { private Integer id; private String name; private String password; }
SysRole 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.jelly.security.bean; import lombok.Data; import java.io.Serializable; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 18:54 **/ @Data public class SysRole { private Integer id; private String name; }
SysUserRole 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.jelly.security.bean; import lombok.Data; import java.io.Serializable; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 18:55 **/ @Data public class SysUserRole { private Integer userId; private Integer roleId; }
Dao SysUserMapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.jelly.security.dao; import com.jelly.security.bean.SysUser; import org.apache.ibatis.annotations.Select; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 19:03 **/ public interface SysUserMapper { @Select("SELECT * FROM sys_user WHERE id = #{id}") SysUser selectById(Integer id); @Select("SELECT * FROM sys_user WHERE name = #{name}") SysUser selectByName(String name); }
SysRoleMapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.jelly.security.dao; import com.jelly.security.bean.SysRole; import org.apache.ibatis.annotations.Select; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 19:07 **/ public interface SysRoleMapper { @Select("SELECT * FROM sys_role WHERE id = #{id}") SysRole selectById(Integer id); }
SysUserRoleMapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.jelly.security.dao; import com.jelly.security.bean.SysUserRole; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 19:08 **/ public interface SysUserRoleMapper { @Select("SELECT * FROM sys_user_role WHERE user_id = #{userId}") List<SysUserRole> listByUserId(Integer userId); }
Service SysUserService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.jelly.security.service; import com.jelly.security.bean.SysUser; import com.jelly.security.dao.SysUserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 19:09 **/ @Service public class SysUserService { @Autowired private SysUserMapper userMapper; public SysUser selectById(Integer id) { return userMapper.selectById(id); } public SysUser selectByName(String name) { return userMapper.selectByName(name); } }
SysRoleService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.jelly.security.service; import com.jelly.security.bean.SysRole; import com.jelly.security.dao.SysRoleMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 19:10 **/ @Service public class SysRoleService { @Autowired private SysRoleMapper roleMapper; public SysRole selectById(Integer id){ return roleMapper.selectById(id); } }
SysUserRoleService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.jelly.security.service; import com.jelly.security.bean.SysUserRole; import com.jelly.security.dao.SysUserRoleMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 19:11 **/ @Service public class SysUserRoleService { @Autowired private SysUserRoleMapper userRoleMapper; public List<SysUserRole> listByUserId(Integer userId) { return userRoleMapper.listByUserId(userId); } }
Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.jelly.security.web; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; /** * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 19:12 **/ @Controller public class LoginController { private Logger logger = LoggerFactory.getLogger(LoginController.class); @RequestMapping("/") public String showHome() { String name = SecurityContextHolder.getContext().getAuthentication().getName(); logger.info("当前登陆用户:" + name); return "home.html"; } @RequestMapping("/login") public String showLogin() { return "login.html"; } @RequestMapping("/admin") @ResponseBody @PreAuthorize("hasRole('ROLE_ADMIN')") public String printAdmin() { return "如果你看见这句话,说明你有ROLE_ADMIN角色"; } @RequestMapping("/user") @ResponseBody @PreAuthorize("hasRole('ROLE_USER')") public String printUser() { return "如果你看见这句话,说明你有ROLE_USER角色"; } }
如代码所示,获取当前登录用户: SecurityContextHolder.getContext().getAuthentication() @PreAuthorize 用于判断用户是否有指定权限,没有就不能访问
配置SpringSecurity UserDetailsService 首先我们需要自定义 UserDetailsService ,将用户信息和权限注入进来。
我们需要重写 loadUserByUsername 方法,参数是用户输入的用户名。返回值是UserDetails,这是一个接口,一般使用它的子类org.springframework.security.core.userdetails.User,它有三个参数,分别是用户名、密码和权限集。
实际情况下,大多将 DAO 中的 User 类继承 org.springframework.security.core.userdetails.User 返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package com.jelly.security.security; import com.jelly.security.bean.SysRole; import com.jelly.security.bean.SysUser; import com.jelly.security.bean.SysUserRole; import com.jelly.security.service.SysRoleService; import com.jelly.security.service.SysUserRoleService; import com.jelly.security.service.SysUserService; 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 java.util.ArrayList; import java.util.Collection; import java.util.List; /** * 我们需要自定义 UserDetailsService ,将用户信息和权限注入进来。 * * 我们需要重写 loadUserByUsername 方法,参数是用户输入的用户名。 * * 返回值是UserDetails,这是一个接口,一般使用它的子类o * * rg.springframework.security.core.userdetails.User, * * 它有三个参数,分别是用户名、密码和权限集。 * @version V1.0 * @author: Jelly * @program: spring-boot-security * @description: * @date: 2019-03-10 19:25 **/ @Service("userDetailsService") public class CustomUserDetailsService implements UserDetailsService { @Autowired private SysUserService userService; @Autowired private SysRoleService roleService; @Autowired private SysUserRoleService userRoleService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Collection<GrantedAuthority> authorities = new ArrayList<>(); // 从数据库中取出用户信息 SysUser user = userService.selectByName(username); // 判断用户是否存在 if(user == null) { throw new UsernameNotFoundException("用户名不存在"); } // 添加权限 List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId()); for (SysUserRole userRole : userRoles) { SysRole role = roleService.selectById(userRole.getRoleId()); authorities.add(new SimpleGrantedAuthority(role.getName())); } // 返回UserDetails实现类 return new User(user.getName(), user.getPassword(), authorities); } }
WebSecurityConfig 该类是 Spring Security 的配置类,该类的三个注解分别是标识该类是配置类、开启 Security 服务、开启全局 Securtiy 注解。
首先将我们自定义的 userDetailsService 注入进来,在 configure() 方法中使用 auth.userDetailsService() 方法替换掉默认的 userDetailsService。
这里我们还指定了密码的加密方式(5.0 版本强制要求设置),因为我们数据库是明文存储的,所以明文返回即可,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package com.jelly.security.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.PasswordEncoder; /** * @version V1.0 * @author: Jelly * @program: * @description: * @date: 2019-03-10 17:27 **/ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService userDetailsService; /** * 想要密码加密 * * auth.userDetailsService(userDetailsService) * .passwordEncoder(new BCryptPasswordEncoder()); * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(charSequence.toString()); } }); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 如果有允许匿名的url,填在下面 // .antMatchers().permitAll() .anyRequest().authenticated() .and() // 设置登陆页 .formLogin().loginPage("/login") // 设置登陆成功页 .defaultSuccessUrl("/").permitAll() // 自定义登陆用户名和密码参数,默认为username和password // .usernameParameter("username") // .passwordParameter("password") .and() .logout().permitAll(); // 关闭CSRF跨域 http.csrf().disable(); } @Override public void configure(WebSecurity web) throws Exception { // 设置拦截忽略文件夹,可以对静态资源放行 web.ignoring().antMatchers("/css/**", "/js/**"); } }
运行程序 在启动类添加注解
1 @MapperScan("com.jelly.security.dao")
启动项目,浏览器输入:localhost:8080。有以下两个角色。
1 2 ROLE_ADMIN 账户:用户名 admin,密码 123 ROLE_USER 账户:用户名 jitwxs,密码 123
配置跨域 在configure中添加
下边添加:
1 2 3 4 5 6 7 8 9 10 11 @Bean public CorsFilter corsFilter() { final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); final CorsConfiguration cors = new CorsConfiguration(); cors.setAllowCredentials(true); cors.addAllowedOrigin("*"); cors.addAllowedHeader("*"); cors.addAllowedMethod("*"); urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", cors); return new CorsFilter(urlBasedCorsConfigurationSource); }