Spring in action读书笔记(七) Spring Security启用权限管理实践
所需jar包
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<servletApiVersion>3.1.0</servletApiVersion>
<springSecurityVersion>3.2.0.RELEASE</springSecurityVersion>
<springVersion>4.0.7.RELEASE</springVersion>
<thymeleafVersion>2.1.3.RELEASE</thymeleafVersion>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${springVersion}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${springVersion}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${springSecurityVersion}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${springSecurityVersion}</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring4</artifactId>
<version>${thymeleafVersion}</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity3</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servletApiVersion}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
一 启用Spring Security最简单的配置
1、MVC启用类,配置thymeleaf
WebInitializer.java
package test; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class<?>[] { RootConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { return new Class<?>[] { WebConfig.class }; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } }
WebConfig.java
package test; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.thymeleaf.extras.springsecurity3.dialect.SpringSecurityDialect; import org.thymeleaf.spring4.SpringTemplateEngine; import org.thymeleaf.spring4.view.ThymeleafViewResolver; import org.thymeleaf.templateresolver.ServletContextTemplateResolver; import org.thymeleaf.templateresolver.TemplateResolver; @Configuration @EnableWebMvc @ComponentScan("test") public class WebConfig extends WebMvcConfigurerAdapter { @Bean public ViewResolver viewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setTemplateEngine(templateEngine); return viewResolver; } @Bean public SpringTemplateEngine templateEngine(TemplateResolver templateResolver) { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver); templateEngine.addDialect(new SpringSecurityDialect()); return templateEngine; } @Bean public TemplateResolver templateResolver() { TemplateResolver templateResolver = new ServletContextTemplateResolver(); templateResolver.setPrefix("/WEB-INF/templates/"); templateResolver.setSuffix(".html"); templateResolver.setTemplateMode("HTML5"); return templateResolver; } }
RootConfig.java
package test; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @Configuration @ComponentScan(basePackages={"test"}, excludeFilters={ @Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class) }) public class RootConfig { }
2、过滤web请求,将web请求委托给Spring Security相关的Filter
package test; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer { }
3、web应用启用Spring Security
package test; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; @Configuration @EnableWebMvcSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { }
访问http://localhost:8080/会链接到http://localhost:8080/login,即Spring security提供的基本登录也功能。默认要求所有的http请求都要认证。
相当于SecurityConfig类重写configure(HttpSecurity)方法
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); }
二 基于内存用户认证
基于内存配置用户,SecurityConfig中重写configure(AuthenticationManagerBuilder)方法,并且配置除首页外其他路径都需要进行认证。
package test; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.Objects; @Configuration @EnableWebMvcSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/home", "/").permitAll() .anyRequest().authenticated() .and() //启用默认登录页面 .formLogin().and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() //密码加密方法,这里简单地反转字符串, 设置的密码是加密后的密码, 因此可使用 user作为用户名、password作为密码进行登录 .passwordEncoder(new ReversePasswordEncoder()) .withUser("user").password(StringUtils.reverse("password")).authorities("ROLE_USER") .and() .withUser("admin").password(StringUtils.reverse("password")).authorities("ROLE_USER", "ROLE_ADMIN"); } private static class ReversePasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { return StringUtils.reverse(String.valueOf(rawPassword)); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return Objects.equals(encode(rawPassword), encodedPassword); } } }
配置controller,创建可访问的html页面
package test; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import static org.springframework.web.bind.annotation.RequestMethod.GET; @Controller @RequestMapping("/") public class HomeController { @RequestMapping(method = GET, value = {"", "/home"}) public String home(Model model) { return "home"; } @RequestMapping(method = GET, value = "/authenticate") public String authenticate() { return "authenticate"; } }
在webapp/WEB-INF/templates目录(与WebConfig中TemplateResolver bean相对应)下新建home.html、authenticate.html文件
home.html
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Home</title> </head> <body> Home Page </body> </html>
authenticate.html
<!DOCTYPE html> <html lang="en"> <head> <title>Title</title> </head> <body> Authenticated </body> </html>
启动Spring应用, 不登录时可访问http://localhost:8080、http://localhost:8080/home,访问http://localhost:8080/authenticate 会跳转到登录页面,
登录后访问 http://localhost:8080/authenticate (登录用户名为user、admin,密码为password。localhost:8080/logout 会退出登录)
三 用户认证、权限管理及自定义用户服务(如从Redis等NoSql数据库中取用户数据)
修改HomeController类,增加html页面,并创建admin.html文件
package test; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import static org.springframework.web.bind.annotation.RequestMethod.GET; @Controller @RequestMapping("/") public class HomeController { @RequestMapping(method = GET, value = {"", "/home"}) public String home(Model model) { return "home"; } @RequestMapping(method = GET, value = "/authenticate") public String authenticate() { return "authenticate"; } @RequestMapping(method = GET, value = "/admin") public String admin() { return "admin"; } }
admin.html
<!DOCTYPE html> <html lang="en"> <head> <title>Admin</title> </head> <body> Admin </body> </html>
修改SecurityConfig类,配置页面访问所需权限,首页home.html允许所有用户访问,admin.html允许ADMIN用户访问,其他页面允许认证用户访问
package test; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.Objects; @Configuration @EnableWebMvcSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/home", "/").permitAll() .antMatchers("/admin").hasAuthority("ROLE_ADMIN") .anyRequest().authenticated() .and() //启用默认登录页面 .formLogin().and() .httpBasic(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定义的用户服务 auth.userDetailsService(new UserDetailServiceImpl()).passwordEncoder(new ReversePasswordEncoder()); } private static class ReversePasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { return StringUtils.reverse(String.valueOf(rawPassword)); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return Objects.equals(encode(rawPassword), encodedPassword); } } }
其中UserDetailServiceImpl类如下,配置了两个用户数据(用户数据可从数据库中获取)
package test; import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.GrantedAuthority; 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 java.util.Arrays; public class UserDetailServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if ("user".equals(username)) { return new User("user", StringUtils.reverse("password"), Arrays.asList(new Role("ROLE_USER"))); } else if ("admin".equals(username)) { return new User("admin", StringUtils.reverse("password"), Arrays.asList(new Role("ROLE_USER"), new Role("ROLE_ADMIN"))); } throw new UsernameNotFoundException(String.format("User '%s' no found", username)); } private static class Role implements GrantedAuthority { private String role; private Role(String role) { this.role = role; } @Override public String getAuthority() { return String.valueOf(role); } } }
启动应用程序,其中admin账号(密码password)可以访问 http://localhost:8080/admin、http://localhost:8080/authenticate、http://localhost:8080/home,
user账号(密码password)可以访问http://localhost:8080/authenticate、http://localhost:8080/home, 访问http://localhost:8080/admin 会报403错误。
不登录情况下只能访问http://localhost:8080/home
四 自定义登录页面
修改WebConfig类,增加/login对应的视图
package test; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.thymeleaf.extras.springsecurity3.dialect.SpringSecurityDialect; import org.thymeleaf.spring4.SpringTemplateEngine; import org.thymeleaf.spring4.view.ThymeleafViewResolver; import org.thymeleaf.templateresolver.ServletContextTemplateResolver; import org.thymeleaf.templateresolver.TemplateResolver; @Configuration @EnableWebMvc @ComponentScan("test") public class WebConfig extends WebMvcConfigurerAdapter { @Bean public ViewResolver viewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setTemplateEngine(templateEngine); return viewResolver; } @Bean public SpringTemplateEngine templateEngine(TemplateResolver templateResolver) { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver); templateEngine.addDialect(new SpringSecurityDialect()); return templateEngine; } @Bean public TemplateResolver templateResolver() { TemplateResolver templateResolver = new ServletContextTemplateResolver(); templateResolver.setPrefix("/WEB-INF/templates/"); templateResolver.setSuffix(".html"); templateResolver.setTemplateMode("HTML5"); return templateResolver; } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login"); } }
新建thymeleaf模板HTML: home.html及page.html
home.html
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Test-login</title> </head> <body onload='document.f.username.focus();'> <div id="header" th:include="page :: header"></div> <div id="content"> <form name='f' th:action='@{/login}' method='POST'> <table> <tr> <td>User:</td> <td> <input type='text' name='username' value=''/></td> </tr> <tr> <td>Password:</td> <td><input type='password' name='password'/></td> </tr> <tr> <td colspan='2'> <input id="remember_me" name="remember-me" type="checkbox"/> <label for="remember_me" class="inline">Remember me</label></td> </tr> <tr> <td colspan='2'> <input name="submit" type="submit" value="Login"/></td> </tr> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> </table> </form> </div> <div id="footer" th:include="page :: copy"></div> </body> </html>
page.html
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <body> <div th:fragment="header"> <a th:href="@{/logout}">Logout</a> </div> <div>Content goes here</div> <div th:fragment="copy">Copyright © Craig Walls</div> </body> </html>
修改SecurityConfig类的configure(HttpSecurity)方法, 设置自定义的登录页面
package test;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import java.util.Objects;
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/home", "/", "/login").permitAll()
.antMatchers("/admin").hasAuthority("ROLE_ADMIN")
.anyRequest().authenticated()
.and()
//设置自定义登录页面
.formLogin().loginPage("/login").and()
//设置退出后重定向页面
.logout().logoutSuccessUrl("/").and()
//启用http basic认证
.httpBasic().and()
//启用remember me 功能, 会在浏览器的cookie中增加一个token, 该token包含用户名、密码、过期时间和一个私钥
.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl())
.tokenValiditySeconds(2419200)
.key("test");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new UserDetailServiceImpl()).passwordEncoder(new ReversePasswordEncoder());
}
private static class ReversePasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return StringUtils.reverse(String.valueOf(rawPassword));
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return Objects.equals(encode(rawPassword), encodedPassword);
}
}
}
重启程序,访问没有权限的页面时会跳转到自定义的登录页面