文章目录
一、SpringSecurity Web权限方案
1. 设置登录系统的账号、密码
方式一:通过配置文件
spring.security.user.name=admin
spring.security.user.password=123
方式二:通过配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
}
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
以上两种方式不常用,因为将用户名和密码写死在代码中了,在实际开发中,我们都是通过数据库进行保存用户名和密码
方式三:自定义编写类实现接口
1.创建配置类,设置userDetailsService接口的实现类
2.编写实现类,返回User对象,User对象有用户名、密码、操作权限
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
@Service("userDetailsService")// 该bean的名称对应上面注入的userDetailsService
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用usersMapper方法,根据用户名查询数据库
LambdaQueryWrapper<Users> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(Users::getUsername, username);
Users users = usersMapper.selectOne(queryWrapper);
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
// 从查询数据库返回users对象,得到用户名和密码,返回
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths);
}
}
2. 实现数据库认证来完成用户登录
(1)准备sql
CREATE TABLE users ( id BIGINT PRIMARY KEY auto_increment, username VARCHAR ( 20 ) UNIQUE NOT NULL, PASSWORD VARCHAR ( 100 ) );-- 密码123
INSERT INTO users
VALUES
( 1, ‘张san’, ‘$2a 10 10 10JgXa4eQLJSuapBFUklLIQ.oIA8WQsV.L8IGKvGEU04fI0WzPBLZBi’ );-- 密码123
INSERT INTO users
VALUES
( 2, ‘李si’, ‘$2a 10 10 10JgXa4eQLJSuapBFUklLIQ.oIA8WQsV.L8IGKvGEU04fI0WzPBLZBi’ );
CREATE TABLE role ( id BIGINT PRIMARY KEY auto_increment, NAME VARCHAR ( 20 ) );
INSERT INTO role
VALUES
( 1, ‘管理员’ );
INSERT INTO role
VALUES
( 2, ‘普通用户’ );
CREATE TABLE role_user ( uid BIGINT, rid BIGINT );
INSERT INTO role_user
VALUES
( 1, 1 );
INSERT INTO role_user
VALUES
( 2, 2 );
CREATE TABLE menu ( id BIGINT PRIMARY KEY auto_increment, NAME VARCHAR ( 20 ), url VARCHAR ( 100 ), parentid BIGINT, permission VARCHAR ( 20 ) );
INSERT INTO menu
VALUES
( 1, ‘系统管理’, ‘’, 0, ‘menu:system’ );
INSERT INTO menu
VALUES
( 2, ‘用户管理’, ‘’, 0, ‘menu:user’ );
CREATE TABLE role_menu ( mid BIGINT, rid BIGINT );
INSERT INTO role_menu
VALUES
( 1, 1 );
INSERT INTO role_menu
VALUES
( 2, 1 );
INSERT INTO role_menu
VALUES
( 2, 2 );
(2)添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
(3)编写实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
private Integer id;
private String username;
private String password;
}
(4)整合MybatisPlus编写mapper
@Repository
public interface UsersMapper extends BaseMapper<Users> {
}
// 扫描Mapper
@SpringBootApplication
@MapperScan("com.lwk.spring_security_web.mapper")
public class SpringSecurityWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityWebApplication.class, args);
}
}
(5)配置文件添加数据库配置
server.port=8111
#mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_security_web?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
(6)编写登录实现类
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用usersMapper方法,根据用户名查询数据库
LambdaQueryWrapper<Users> queryWrapper = new LambdaQueryWrapper();
// where username=?
queryWrapper.eq(Users::getUsername, username);
Users users = usersMapper.selectOne(queryWrapper);
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
// 从查询数据库返回users对象,得到用户名和密码,返回
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths);
}
}
(7)编写Controller
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("hello")
public String hello() {
return "hello security";
}
}
(8)测试访问
没认证前,访问http://localhost:8111/test/hello,会跳转到SpringSecurity默认的登录页面
认证成功后跳转到访问页面
3. 自定义配置
在之前的自定义实现类中加 configure(HttpSecurity http) 方法,可以自定义一些配置
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用usersMapper方法,根据用户名查询数据库
LambdaQueryWrapper<Users> queryWrapper = new LambdaQueryWrapper();
// where username=?
queryWrapper.eq(Users::getUsername, username);
Users users = usersMapper.selectOne(queryWrapper);
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
// 从查询数据库返回users对象,得到用户名和密码,返回
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置退出url,退出成功后跳转到指定页面
http.logout().logoutUrl("/logout").
logoutSuccessUrl("/test/hello").permitAll();
// 若没有权限访问,则跳转到自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
http.formLogin() // 跳转到自定义的登录页面
.loginPage("/login.html") // 登录页面设置
.loginProcessingUrl("/user/login") // 登录访问路径
.defaultSuccessUrl("/success.html").permitAll() // 登录成功之后,跳转路径
.failureUrl("/unauth.html")
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证
.and().csrf().disable(); // 关闭csrf防护
}
}
4. 基于角色或权限访问控制
(1)hasAuthority 方法
如果当前的主体具有指定的权限,则返回 true,否则返回 false
1.在配置类设置当前访问地址有哪些权限
.antMatchers(“/test/index”).hasAuthority(“admins”) // 当前登录用户,只有具有admins权限才可以访问该路径
2.在UserDetailsService实现类中,给返回的User对象设置权限
List< GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(“admin”);
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()),auths);
(2)hasAnyAuthority 方法
如果当前的主体有任何提供的权限(给定的作为一个逗号分隔的字符串列表),返回true.
1.在配置类设置当前访问地址有哪些权限
.antMatchers(“/test/index”).hasAnyAuthority(“admins,manager”)// 当前登录用户,具有有任何提供的权限都可以访问该路径
2.在UserDetailsService实现类中,给返回的User对象设置权限
List< GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(“admin”);
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()),auths);
(3)hasRole 方法
如果当前主体具有指定的角色,则返回true
源码:
校验角色时,底层会带上ROLE前缀与设置的角色进行匹配
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
} else {
return "hasRole('ROLE_" + role + "')";
}
}
1.在配置类设置当前访问地址有哪些角色(注意配置文件中不需要添加”ROLE_“,因为上述的底层代码会自动添加与之进行匹配)
.antMatchers(“/test/index”).hasRole(“sale”) // 当前登录用户,只有具有sale角色才可以访问该路径
2.在UserDetailsService实现类中,给返回的User对象设置权限和角色
List< GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(“admin,ROLE_sale”);
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()),auths);
(4)hasAnyRole 方法
表示用户具备任何一个角色都可以访问
具体使用与前面类似,这里不再赘述
5. 自动登录-RememberMe(“记住我”功能)
(1)自动登录实现原理
1.通过浏览器访问认证成功后,服务端会向浏览器发送加密的字符串,浏览器通过cookie将该信息保存;服务端还会将加密的字符串和对应的用户信息保存到数据库;
2.浏览器再次访问时,会携带cookie信息传给服务端,服务端拿着cookie信息去数据库比对,能查到对应的信息,则认证成功,不用再返回登录页输入用户名和密码,实现了自动登录。
(2)SpringSecurity自动登录实现原理
UsernamePasswordAuthenticationFilter通过attemptAuthentication方法认证成功后,
调用父类AbstractAuthenticationProcessingFilter中doFilter方法,
再调用doFilter中的successfulAuthentication方法,
在successfulAuthentication中调用rememberMeServices.loginSuccess()方法
最后在PersistentTokenBasedRememberMeServices中调用onLoginSuccess方法
在onLoginSuccess中执行tokenRepository.createNewToken(persistentToken);生成token,addCookie(persistentToken, request, response);将token写入浏览器的cookie中,在PersistentTokenRepository的实现类JdbcTokenRepositoryImpl中,将token写入数据库
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
tokenRepository.createNewToken(persistentToken);
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
// ~ Static fields/initializers
// =====================================================================================
/** Default SQL for creating the database table to store the tokens */
public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
+ "token varchar(64) not null, last_used timestamp not null)";
/** The default SQL used by the <tt>getTokenBySeries</tt> query */
public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
/** The default SQL used by <tt>createNewToken</tt> */
public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
/** The default SQL used by <tt>updateToken</tt> */
public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
/** The default SQL used by <tt>removeUserTokens</tt> */
public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
}
再次进行服务请求时,调用RememberMeAuthenticationFilter中的doFilter
然后执行doFilter中的Authentication rememberMeAuth = rememberMeServices.autoLogin(request,response);进行自动登录
在autoLogin方法中,通过cookie获取token,将token解析后的user信息与数据库进行比对,若一致就自动登录
// 简化后的autoLogin方法
@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
String rememberMeCookie = extractRememberMeCookie(request);
logger.debug("Remember-me cookie detected");
UserDetails user = null;
String[] cookieTokens = decodeCookie(rememberMeCookie);
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
return createSuccessfulAuthentication(request, user);
}
(3)SpringSecurity自动登录实现步骤
- 在配置类中,注入 数据源,配置操作数据库对象
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
//注入数据源
@Autowired
private DataSource dataSource;
//配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
- 在配置类中,配置自动登录
// 开启记住我功能
http.rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)//设置有效时长,单位秒
.userDetailsService(userDetailsService);
- 页面添加记住我复选框
记住我:<input type="checkbox"name="remember-me"title=“记住密码”/>
此处:name 属性值必须位 remember-me.不能改为其他值
6. CSRF
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。