赞
踩
Spring Security的安全管理两个重要概念是:认证(Authentication)和授权(Authorization)。认证即对用户登录进行管控,授权即对登录用户的权限进行管控。
添加spring-boot-starter-security启动器,pom中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
一旦项目中引入spring-boot-starter-security启动器,安全功能会立即生效,启动项目后控制台会打印Using generated security password:...
,这个即为默认密码,默认用户名为“user”。登录首页会跳转到自带的登录页面,输入该账号密码即可进入。
通过自定义WebSecurityConfigurerAdapter类型的Bean组件重写void configure(AuthenticationManagerBuilder auth)方法,实现自定义用户认证。
Spring Security提供了多种自定一认证方式:
最简单的认证方式,一般用于测试,需要重写configure(AuthenticationManagerBuilder auth)方法。
package com.xc.config; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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; /** * @author wyp */ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //设置密码编码器 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); //设置内存用户信息 auth.inMemoryAuthentication().passwordEncoder(encoder) .withUser("zs").password(encoder.encode("123456")).roles("common") .and() .withUser("ls").password(encoder.encode("123456")).roles("vip"); } }
- @EnableWebSecurity注解开启MVC Security安全支持,等同于@EnableGlobalAuthentication、@Configuration、@Import的组合使用。
- 从Spring Security 5开始,自定义用户认证必须设置密码编码器用于保护密码,否则会异常。
- Spring Security有多种编码器,BCryptPasswordEncoder、Pbkdf2PasswordEncoder、SCryptPasswordEncoder等。
- 自定义用户时可定义用户角色roles和权限authorities,也可以一次添加多个角色或权限,如
roles("common","vip")
和authorities("ROLE_common","ROLE_vip")
是等效的。
创建三个表customer_tb(用户表)、authority_tb(权限表)、customer_authority_tb(用户权限关联表)。
CREATE TABLE `authority_tb` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`authority` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `authority_tb` VALUES (1, 'ROLE_common');
INSERT INTO `authority_tb` VALUES (2, 'ROLE_vip');
SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `customer_tb` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`password` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`valid` tinyint(1) NOT NULL DEFAULT 1 COMMENT '校验用户是否合法,默认合法1',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `customer_tb` VALUES (1, 'zs', '$2a$10$X5/MLB1vMYOAF9./ib9aROrmeaoBLuvHxSw9XPoMLDJCgrjInofty', 1);
INSERT INTO `customer_tb` VALUES (2, 'ls', '$2a$10$X5/MLB1vMYOAF9./ib9aROrmeaoBLuvHxSw9XPoMLDJCgrjInofty', 1);
SET FOREIGN_KEY_CHECKS = 1;
CREATE TABLE `customer_authority_tb` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`customer_id` int(10) NULL DEFAULT NULL COMMENT '用户id',
`authority_id` int(10) NULL DEFAULT NULL COMMENT '权限id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `customer_authority_tb` VALUES (1, 1, 1);
INSERT INTO `customer_authority_tb` VALUES (2, 2, 2);
SET FOREIGN_KEY_CHECKS = 1;
注意: 由于Security进行用户查询时是先通过username定位用户是否唯一的,所以customer_tb的username字段必须唯一;customer_tb必须定义一个tinyint类型的字段(对应boolean类型的属性,如上面创建的valid),用于校验用户身份是否合法;插入的密码必须对应编码器编码后的密码;customer_tb的权限值必须带有“ROLE_”前缀,而默认的用户角色值则是对应权限去掉前缀“ROLE_”。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
此处省略全局配置…
package com.xc.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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 javax.sql.DataSource; /** * 注解:@EnableWebSecurity-开启MVC Security安全支持 * @author wyp */ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //设置密码编码器 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); //使用JDBC进行身份验证 String userSql = "select username,password,valid from customer_tb where username = ?"; String authoritySql = "select c.username,a.authority " + "from customer_tb c,authority_tb a,customer_authority_tb ca " + "where ca.customer_id=c.id and ca.authority_id=a.id and c.username=?"; auth.jdbcAuthentication().passwordEncoder(encoder) .dataSource(dataSource) .usersByUsernameQuery(userSql) .authoritiesByUsernameQuery(authoritySql); } }
- 定义用户查询的sql语句时,必须返回用户名username、密码password、是否为有效用户valid三个字段信息;
- 定义权限查询的sql语句时,必须返回用户名username、权限authority两个字段信息;
- usersByUsernameQuery(String query)设置用于通过用户名查找用户的查询;
- authoritiesByUsernameQuery(String query)设置用于通过用户名查找用户权限的查询。
频繁的使用jdbc进行数据查询认证麻烦且会降低网站的响应速度,推荐使用UserDetailsService身份认证。
如某些业务已经实现了用户信息的查询服务,就没必要再使用JDBC进行身份认证了。
CustomerServiceImpl.java
package com.xc.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.xc.entity.Authority; import com.xc.entity.Customer; import com.xc.entity.CustomerAuthority; import com.xc.mapper.AuthorityMapper; import com.xc.mapper.CustomerAuthorityMapper; import com.xc.mapper.CustomerMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; /** * @author wyp */ @Service public class CustomerServiceImpl implements CustomerService{ @Autowired private CustomerMapper customerMapper; @Autowired private AuthorityMapper authorityMapper; @Autowired private CustomerAuthorityMapper customerAuthorityMapper; @Resource private RedisTemplate<String,Object> redisTemplate; @Override public Customer findCustomerByName(String username) { Customer customer; Object o = redisTemplate.opsForValue().get("customer::" + username); if (o != null) { customer = (Customer) o; }else { QueryWrapper<Customer> customerQueryWrapper = new QueryWrapper<>(); customerQueryWrapper.eq("username",username); customer = customerMapper.selectOne(customerQueryWrapper); if (customer != null) { redisTemplate.opsForValue().set("customer::" + username,customer); } } return customer; } @Override public List<Authority> findAuthorityByName(String username) { List<Authority> authorityList; Object o = redisTemplate.opsForValue().get("authorityList::" + username); if (o != null) { authorityList = (List<Authority>) o; }else { authorityList = ...; //此处省略查询步骤 if (authorityList.size()>0) { redisTemplate.opsForValue().set("authorityList::" + username,authorityList); } } return authorityList; } }
UserDetailsService是Security提供的用于封装认证用户信息的接口,通过重写loadUserByUsername(String username)方法通过用户名加载用户信息。
package com.xc.config.service; import com.xc.entity.Authority; import com.xc.entity.Customer; import com.xc.service.CustomerService; import org.springframework.beans.factory.annotation.Autowired; 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.List; import java.util.stream.Collectors; /** * @author wyp */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private CustomerService customerService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //获取用户和权限信息 Customer customer = customerService.findCustomerByName(username); //如果用户不存在,必须抛出异常,否则会导致整体报错 if (customer == null) { throw new UsernameNotFoundException("当前用户不存在"); } List<Authority> authorityList = customerService.findAuthorityByName(username); //对用户权限进行封装 List<SimpleGrantedAuthority> list = authorityList.stream() .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList()); //返回封装的UserDetails用户详情类 return new User(customer.getUsername(), customer.getPassword(), list); } }
package com.xc.config; import com.xc.config.service.UserDetailsServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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; /** * 注解:@EnableWebSecurity-开启MVC Security安全支持 * @author wyp */ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //设置密码编码器 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); //使用userDetailsService进行身份认证 auth.userDetailsService(userDetailsService).passwordEncoder(encoder); } }
HttpSecurity类的主要方法及说明
方法 | 说明 |
---|---|
authorizeRequests() | 开启基于HttpServletRequest请求访问的限制 |
formLogin() | 开启基于表单的用户登录 |
httpBasic() | 开启基于HTTP请求的Basic认证登录 |
logout() | 开启退出登录的支持 |
sessionManagement() | 开启Session管理配置 |
rememberMe() | 开启记住我功能 |
csrf() | 配置CSRF跨站请求伪造防护功能 |
通过authorizeRequests()开启
用户访问控制主要方法及说明
方法 | 描述 |
---|---|
antMatchers(String… antPatterns) | 开启Ant风格的路径匹配 |
mvcMatchers(String… mvcPatterns) | 开启MVC风格的路径匹配 |
regexMatchers(String… regexPatterns) | 开启正则表达式的路径匹配 |
and() | 功能连接符 |
anyRequest() | 匹配任何请求 |
rememberMe() | 开启记住我功能 |
access(String attribute) | 匹配给定的SpEL表达式计算结果是否为true |
hasAnyRole(String… authorities) | 匹配用户是否有参数中的任意角色 |
hasRole(String role) | 匹配用户是否有某一个角色 |
hasAnyAuthority(String… authorities) | 匹配用户是否有参数中的任意权限 |
hasAuthority(String authority) | 匹配用户是否有某一权限 |
authenticated() | 匹配已经登录认证的用户 |
fullyAuthenticated() | 匹配完整登录认证的用户,而非记住我登录用户 |
hasIpAddress(String ipaddressExpression) | 匹配某IP地址的请求访问 |
permitAll() | 无条件对请求进行放行 |
示例:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
//其他请求要求用户必须进行登录认证
.anyRequest().authenticated()
.and()
.formLogin();
}
}
之前一直使用的自带的登录界面,但通常登录界面要自己定制。
通过formLogin()开启。
用户登录主要方法及说明
方法 | 描述 |
---|---|
loginPage(String loginPage) | 用户登录页面跳转路径,默认为get请求的/login |
successForwardUrl(String forwardUrl) | 用户登录成功后的重定向地址 |
successHandler(AuthenticationSuccessHandler successHandler) | 用户登录成功后的处理 |
defaultSuccessUrl(String defaultSuccessUrl) | 用户直接登录后默认跳转地址 |
failureForwardUrl(String forwardUrl) | 用户登录失败后的重定向地址 |
failureUrl(String authenticationFailureUrl) | 用户登录失败后的跳转地址,默认/login?error |
failureHandler(AuthenticationFailureHandler authenticationFailureHandler) | 用户登录失败后的处理结果 |
usernameParameter(String usernameParameter) | 登录用户的用户名参数,默认username |
passwordParameter(String passwordParameter) | 登录用户的密码参数,默认password |
loginProcessingUrl(String loginProcessingUrl) | 登录表单提交的路径,默认为post请求的/login |
permitAll() | 无条件对请求进行放行 |
resources目录的templates下新建login目录,创建login.html,静态资源(图片css等)放在static/login下
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登录</title> <link rel="stylesheet" th:href="@{/login/css/login.css}"> </head> <body th:style="'background-image: url(/login/img/back.jpg)'"> <div> <form th:action="@{/userLogin}" method="post" style="width: 500px;margin: 200px auto;border: 2px gray solid;text-align: center"> <h2>登录</h2> <!--错误信息登录提示框--> <div th:if="${param.error}" style="color: red">用户名或密码错误</div> <label> <input type="text" name="name" placeholder="用户名"> </label><br> <label> <input type="text" name="password" placeholder="密码"> </label><br> <div> <label> <input type="checkbox" value="remember-me"> </label>记住我 </div><br> <input type="submit" value="登录"><br> </form> </div> </body> </html>
其中,登录提交方式必须是post,th:if="${param.error}"判断请求中是否带有error参数,该参数的Security默认的,用户也可以自定义。
定义登录跳转控制层
@GetMapping("/userLogin")
public String toLoginPage() {
return "login/login";
}
然后定义用户登录控制,重写configure(HttpSecurity http)方法:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//放行静态资源
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated();
http.formLogin()
.loginPage("/userLogin").permitAll()
.loginProcessingUrl("/userLogin")
.usernameParameter("name").passwordParameter("password")
.defaultSuccessUrl("/")
.failureUrl("/userLogin?error");
}
注意:
使用HttpSecurity类的logout()方法处理用户退出,会清除Session和记住我等任何默认用户配置。
用户退出主要方法及说明
方法 | 说明 |
---|---|
logoutUrl(String logoutUrl) | 用户退出处理控制URL,默认post请求的/logout |
logoutSuccessUrl(String logoutSuccessUrl) | 用户退出成功后的重定向地址 |
logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) | 用户退出成功后的处理器设置 |
deleteCookies(String… cookieNamesToClear) | 用户退出后删除指定的Cookie |
invalidateHttpSession(boolean invalidateHttpSession) | 用户退出后是否立即清除Session,默认true |
clearAuthentication(boolean clearAuthentication) | 用户退出后是否立即清除Authentication用户认证信息,默认true |
首页添加注销按钮:
<form th:action="@{/myLogout}" method="post">
<input type="submit" value="注销">
</form>
在configure(HttpSecurity http)方法中新增退出:
http.logout()
.logoutUrl("/myLogout")
.logoutSuccessUrl("/");
获取用户信息通常用以下两种方式:HttpSession和SecurityContextHolder。(需要关闭csrf防护http.csrf().disable();
)
使用HttpSession获取用户信息:
@GetMapping("/getUserBySession") public void getUser(HttpSession session) { // 从当前HttpSession获取绑定到此会话的所有对象的名称 Enumeration<String> names = session.getAttributeNames(); while (names.hasMoreElements()) { // 获取HttpSession中会话名称 String element = names.nextElement(); // 获取HttpSession中的应用上下文 SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element); System.out.println("element: " + element); System.out.println("attribute: " + attribute); // 获取用户相关信息 Authentication authentication = attribute.getAuthentication(); UserDetails principal = (UserDetails) authentication.getPrincipal(); System.out.println(principal); System.out.println("username: " + principal.getUsername()); } }
会话中有一个key为“SPRING_SECURITY_CONTEXT”的用户信息(即element输出的信息),UserDetails中封装了用户的主要信息,如子用户名、权限等。
使用SecurityContextHolder获取用户信息:
@GetMapping("/getUserByContext")
public void getUserByContext() {
//获取应用上下文
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("context = " + context);
//获取用户相关信息
Authentication authentication = context.getAuthentication();
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
System.out.println("userDetails = " + userDetails);
System.out.println("username = " + userDetails.getUsername());
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。