个人认为,在框架中,最难的就是Spring与鉴权框架。大部分框架,即便不知道原理,知道如何使用,也能完成日常的开发。而鉴权框架和Spring不同,他们并没有限定如何去使用,更多的,需要程序员自己的想法。
我的文章不会写得很细,只会帮你完成一个可以运行的HelloWorld。
Maven依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.13.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.seaboot</groupId> <artifactId>security</artifactId> <version>0.0.1-SNAPSHOT</version> <name>security</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.SR5</spring-cloud.version> </properties> <dependencies> <!--<dependency>--> <!--<groupId>org.springframework.cloud</groupId>--> <!--<artifactId>spring-cloud-starter-oauth2</artifactId>--> <!--</dependency>--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Controller
主要测试如何获取当前登录用户的信息
package cn.seaboot.security.ctrl; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @author Mr.css * @date 2020-05-06 15:06 */ @RestController public class HelloController { @GetMapping("/hello") public String hello() { //获取登录的账号 Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); System.out.println(principal.getClass()); System.out.println(principal); return "hello"; } }
SecurityConfig
主要配置都包含在此接口中,按照自己的实际需求调整即可。
package cn.seaboot.security.config; import org.springframework.context.annotation.Bean; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author Mr.css * @date 2020-05-07 23:38 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 下面这两行配置表示在内存中配置了两个用户,分别是javaboy和lisi,密码都是123,并且赋予了admin权限 * @param auth AuthenticationManagerBuilder */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("javaboy") .roles("admin") .password("$2a$10$Wuts2iHTzQBmeRVKJ21oFuTsvOJ5ffsqpD3DRzNupKwn5Gy54LEpC") .and() .withUser("lisi") .roles("user") .password("$2a$10$gDCkllHpktQfHgwYWKW2T.JCgkUZcTZTLfDBhlJvTLO/BDSMeA2YS"); } /** * 加密算法 */ @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * URL角色权限配置,下列代码的意思是:访问路径hello,需要有admin角色 * * @param http HttpSecurity */ @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/hello/**").hasRole("admin") .antMatchers("/hello").hasRole("admin") .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } /** * 白名单配置:直接过滤掉该地址,即该地址不走 Spring Security 过滤器链 */ @Override public void configure(WebSecurity web){ web.ignoring().antMatchers("/vercode"); } /** * 测试加密算法 * @param args */ public static void main(String[] args) { System.out.println(new BCryptPasswordEncoder().encode("123"));; System.out.println(new BCryptPasswordEncoder().matches("123", "$2a$10$gDCkllHpktQfHgwYWKW2T.JCgkUZcTZTLfDBhlJvTLO/BDSMeA2YS"));; } }
URL权限配置的其它可选项:
在configure函数中,已经展示了如何给Url配置权限,更多的配置如下:
antMatchers(url).hasRole()
antMatchers(url).access()
hasRole([role]) 当前用户是否拥有指定角色。
hasAnyRole([role1,role2]) 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。
hasAuthority([auth]) 等同于hasRole
hasAnyAuthority([auth1,auth2]) 等同于hasAnyRole
Principle 代表当前用户的principle对象
authentication 直接从SecurityContext获取的当前Authentication对象
permitAll 总是返回true,表示允许所有的
denyAll 总是返回false,表示拒绝所有的
isAnonymous() 当前用户是否是一个匿名用户
isRememberMe() 表示当前用户是否是通过Remember-Me自动登录的
isAuthenticated() 表示当前用户是否已经登录认证成功了。
isFullyAuthenticated() 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。
测试
访问Hello地址,就会自动跳转登录页面(Spring Security内置),用自己配置的账号即可登录。
问题一
思考问题:我们的用户肯定是配置在数据库里的,登录页面也必定是用自己的,上述代码肯定不满足我们日常使用,我们该怎么编写我们需要的代码?
答:通过formLogin()可以配置我们自己的登录页面,表单提交路径,以及首页地址。
配置如下:
http .formLogin() .loginPage("/login.html") .failureUrl("/login.html?error=1") .defaultSuccessUrl("/index.html") .loginProcessingUrl("/user/login") .permitAll() .and()
登录的接口如下:
package org.springframework.security.core.userdetails; public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }
问题二:
思考问题:上述的接口,只有1个参数 UserName,那么问题就来了,假设我们有2个参数怎么办?比如:验证码。
答:可以添加一个登录前置拦截,先验证验证码的有效性,然后再走我们的正常流程。
http.addFilterBefore(new xxxxxxFilter(), xxxxxxxFilter.class)
SecurityConfig进阶
根据上述问题,对代码进行调整
package cn.seaboot.security.config; import org.springframework.context.annotation.Bean; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import javax.annotation.Resource; import javax.sql.DataSource; /** * @author Mr.css * @date 2020-05-07 23:38 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * @param auth AuthenticationManagerBuilder */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 设置自定义的userDetailsService auth.userDetailsService(new CustomUserDetailsService()) .passwordEncoder(passwordEncoder()); } /** * 加密算法 */ @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * URL角色权限配置,访问路径hello,需要有admin角色 * * @param http HttpSecurity */ @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(new BeforeLoginAuthenticationFilter("/user/login", "/login.html"), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/hello/**").hasRole("admin") .antMatchers("/hello").hasRole("admin") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .failureUrl("/login.html?error=1") .defaultSuccessUrl("/index.html") .loginProcessingUrl("/user/login") .permitAll() .and() .httpBasic(); //session管理 //session失效后跳转到登录页面 http.sessionManagement().invalidSessionUrl("/toLogin"); //单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线 //http.sessionManagement().maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy()); //单用户登录,如果有一个登录了,同一个用户在其他地方不能登录 http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true); //默认的登录页面有一个用于安全验证的token,如果使用模版引擎,可以使用表达式获取,这里直接使用html,因此先禁用 // <input name="_csrf" type="hidden" value="d2ef6916-316b-4889-895c-07a2ca3759fc"> // <input type = “hidden” name = “${_csrf.parameterName}” value = “${_csrf.token}” /> http.csrf().disable(); } /** * 白名单配置:直接过滤掉该地址,即该地址不走 Spring Security 过滤器链 */ @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/vercode"); } }
模拟用户登录
package cn.seaboot.security.config; 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 java.util.ArrayList; import java.util.List; /** * * @author Mr.css * @date 2020-05-08 0:02 */ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { // TODO: 查询账户 // 这里并没有真正去查询数据库,而是允许任意账号登录,密码都是123,并且都是admin角色 // GrantedAuthority直译是授予权限,与config中配置的hasRole有歧义,但是功能上其实是一样的 // 与Shiro不同,在Security中,并没有区分角色和权限 List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_admin"); grantedAuthorities.add(grantedAuthority); return new org.springframework.security.core.userdetails.User(userName,"$2a$10$gDCkllHpktQfHgwYWKW2T.JCgkUZcTZTLfDBhlJvTLO/BDSMeA2YS", grantedAuthorities); } }
模拟验证码校验
import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author Mr.css * @date 2020-05-10 1:14 */ public class BeforeLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private String servletPath; public BeforeLoginAuthenticationFilter(String servletPath, String failureUrl) { super(servletPath); this.servletPath = servletPath; setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(failureUrl)); } @Override public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException { return null; } /** * 这里模拟客户端的验证码,只要验证码是test,即可通过校验 */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; if(servletPath.equals(req.getServletPath()) && "POST".equalsIgnoreCase(req.getMethod())){ if (!"test".equals(req.getParameter("token"))) { unsuccessfulAuthentication(req, (HttpServletResponse) response, new InsufficientAuthenticationException("输入的验证码不正确")); return; } } chain.doFilter(request, response); } }
登录页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 自定义表单验证: <form action="/user/login" method="post"> <br/> 用户名: <input type="text" name="username" placeholder="name"><br/> 密码: <input type="password" name="password" placeholder="password"><br/> <input type="text" name="token" value="test"><br/> <input name="submit" type="submit" value="提交"> </form> </body> </html>
一些其它配置:
//单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
//http.sessionManagement().maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy());
//单用户登录,如果有一个登录了,同一个用户在其他地方不能登录
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
默认的登录过滤器:
UsernamePasswordAuthenticationFilter