Spring Security 入门篇

本文是一个笔记系列,目标是完成一个基于角色的权限访问控制系统(RBAC),有基本的用户、角色、权限管理,重点在Spring Security的各种配置 。万丈高楼平地起,接下来,一步一步,由浅入深,希望给一起学习的小伙伴一个参考 。
1.  Hello Security
按照惯例,先写个Hello World
首先,引入依赖
1 <dependency>2<groupId>org.springframework.boot</groupId>3<artifactId>spring-boot-starter-security</artifactId>4 </dependency>先来理清楚“认证”和“授权”两个概念 。认证就是告诉我你是谁,授权就是你可以做什么 。结合实际项目通俗地来讲,认证就是登录,授权就是访问资源 。故而,我们需要先有用户和资源,先简单地定义几个内存用户和资源吧,为此需要在WebSecurtiyConfigurerAdapter中进行配置 。
WebSecurityConfig.java
1 package com.example.demo.config; 23 import org.springframework.context.annotation.Bean; 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 6 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 import org.springframework.security.crypto.password.PasswordEncoder;10 11 @Configuration12 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {13 14@Override15protected void configure(AuthenticationManagerBuilder auth) throws Exception {16auth.inMemoryAuthentication()17.withUser("zhangsan").password(passwordEncoder().encode("123456")).roles("user")18.and()19.withUser("admin").password(passwordEncoder().encode("123456")).roles("admin")20.and()21.passwordEncoder(passwordEncoder());22}23 24@Override25protected void configure(HttpSecurity http) throws Exception {26http.formLogin()27 //.loginPage("/login.html")28.loginProcessingUrl("/login")29.usernameParameter("username")30.passwordParameter("password")31.defaultSuccessUrl("/")32.and()33.authorizeRequests()34.antMatchers("/login.html", "/login").permitAll()35.antMatchers("/hello/sayHello").hasAnyAuthority("ROLE_user", "ROLE_admin")36.antMatchers("/hello/sayHi").hasAnyRole("admin")37.anyRequest().authenticated();38}39 40@Bean41public PasswordEncoder passwordEncoder() {42return new BCryptPasswordEncoder();43}44 }HelloController.java
1 package com.example.demo.controller; 23 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RestController; 67 @RestController 8 @RequestMapping("/hello") 9 public class HelloController {10 11@GetMapping("/sayHello")12public String sayHello() {13return "hello";14}15 16@GetMapping("/sayHi")17public String sayHi() {18return "hi";19}20 21 }项目结构

Spring Security 入门篇

文章插图
定义了两个用户zhangsan和admin,他们的密码都是123456,zhangsan的角色是user可以访问/hello/sayHello,admin的角色是admin可以访问/hello/sayHello和hello/sayHi
2.  认证成功/失败处理
按照刚才的写法,登录成功之后是跳到/页面,失败跳转到登录页 。但是,对于前后端分离的项目,我希望它返回json数据,而不是重定向到某个页面
处理用户名和密码登录的过滤器是org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter,既然是过滤器,直接看doFilter方法
Spring Security 入门篇

文章插图
不用多说,自定义认证成功处理器 
1 package com.example.demo.handler; 23 import com.fasterxml.jackson.databind.ObjectMapper; 4 import org.springframework.security.core.Authentication; 5 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 6 import org.springframework.stereotype.Component; 78 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServletRequest;10 import javax.servlet.http.HttpServletResponse;11 import java.io.IOException;12 13 @Component14 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {15 16private static ObjectMapper objectMapper = new ObjectMapper();17 18@Override19public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {20response.setContentType("application/json;charset=utf-8");21response.getWriter().write(objectMapper.writeValueAsString("ok"));22}23 }自定义认证失败处理器
1 package com.example.demo.handler; 23 import com.fasterxml.jackson.databind.ObjectMapper; 4 import org.springframework.security.core.AuthenticationException; 5 import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; 6 import org.springframework.stereotype.Component; 78 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServletRequest;10 import javax.servlet.http.HttpServletResponse;11 import java.io.IOException;12 13 @Component14 public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {15 16private static ObjectMapper objectMapper = new ObjectMapper();17 18@Override19public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {20response.setContentType("application/json;charset=utf-8");21response.getWriter().write(objectMapper.writeValueAsString("error"));22}23 }WebSecurityConfig配置
1 package com.example.demo.config; 23 import com.example.demo.handler.MyAuthenticationFailureHandler; 4 import com.example.demo.handler.MyAuthenticationSuccessHandler; 5 import com.example.demo.handler.MyExpiredSessionStrategy; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.context.annotation.Bean; 8 import org.springframework.context.annotation.Configuration; 9 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;10 import org.springframework.security.config.annotation.web.builders.HttpSecurity;11 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;12 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;13 import org.springframework.security.crypto.password.PasswordEncoder;14 15 @Configuration16 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {17 18@Autowired19private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;20@Autowired21private MyAuthenticationFailureHandler myAuthenticationFailureHandler;22 23@Override24protected void configure(AuthenticationManagerBuilder auth) throws Exception {25auth.inMemoryAuthentication()26.withUser("zhangsan").password(passwordEncoder().encode("123456")).roles("user")27.and()28.withUser("admin").password(passwordEncoder().encode("123456")).roles("admin")29.and()30.passwordEncoder(passwordEncoder());31}32 33@Override34protected void configure(HttpSecurity http) throws Exception {35http.formLogin()36 //.loginPage("/login.html")37.loginProcessingUrl("/login")38.usernameParameter("username")39.passwordParameter("password")40 //.defaultSuccessUrl("/")41.successHandler(myAuthenticationSuccessHandler)42.failureHandler(myAuthenticationFailureHandler)43.and()44.authorizeRequests()45.antMatchers("/login.html", "/login").permitAll()46.antMatchers("/hello/sayHello").hasAnyAuthority("ROLE_user", "ROLE_admin")47.antMatchers("/hello/sayHi").hasAnyRole("admin")48.anyRequest().authenticated()49.and()50.sessionManagement().sessionFixation().migrateSession()51.maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());52}53 54@Bean55public PasswordEncoder passwordEncoder() {56return new BCryptPasswordEncoder();57}58 }再多自定义一个Session过期策略,当Session过期或者被踢下线以后的处理逻辑
1 package com.example.demo.handler; 23 import com.fasterxml.jackson.databind.ObjectMapper; 4 import org.springframework.security.web.session.SessionInformationExpiredEvent; 5 import org.springframework.security.web.session.SessionInformationExpiredStrategy; 67 import javax.servlet.ServletException; 8 import javax.servlet.http.HttpServletResponse; 9 import java.io.IOException;10 11 public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {12 13private static ObjectMapper objectMapper = new ObjectMapper();14 15@Override16public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {17String msg = "登录超时或已在另一台机器登录,您被迫下线!";18HttpServletResponse response = event.getResponse();19response.setContentType("application/json;charset=utf-8");20response.getWriter().write(objectMapper.writeValueAsString(msg));21}22 }3.  从数据库中加载用户及权限
刚才用户是在内存中定义的,这肯定是不行的,下面从数据库中加载用户及其所拥有的权限
最简单的结构是这样的:
Spring Security 入门篇

文章插图
为了减少用户的重复授权,引入用户组 。将用户加入用户组以后,就自动拥有组所对应的权限 。
Spring Security 入门篇

文章插图
下面,按照最简单的用户角色权限模型来改造刚才的项目
首先,通过实现UserDetails接口来自定义一个用户信息对象
MyUserDetails.java
1 package com.example.demo.model; 23 import org.springframework.security.core.GrantedAuthority; 4 import org.springframework.security.core.userdetails.UserDetails; 56 import java.util.Collection; 78 public class MyUserDetails implements UserDetails { 9 10private String username;11private String password;12private boolean enabled;13private Collection<? extends GrantedAuthority> authorities;14 15public MyUserDetails(String username, String password, boolean enabled) {16this.username = username;17this.password = password;18this.enabled = enabled;19}20 21public void setUsername(String username) {22this.username = username;23}24 25public void setPassword(String password) {26this.password = password;27}28 29public void setEnabled(boolean enabled) {30this.enabled = enabled;31}32 33public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {34this.authorities = authorities;35}36 37@Override38public Collection<? extends GrantedAuthority> getAuthorities() {39return authorities;40}41 42@Override43public String getPassword() {44return password;45}46 47@Override48public String getUsername() {49return username;50}51 52@Override53public boolean isAccountNonExpired() {54return true;55}56 57@Override58public boolean isAccountNonLocked() {59return true;60}61 62@Override63public boolean isCredentialsNonExpired() {64return true;65}66 67@Override68public boolean isEnabled() {69return enabled;70}71 }有了UserDetails以后,还需要UserDetailsService去加载它,所以自定义一个UserDetailsService
MyUserDetailsService.java
1 package com.example.demo.service; 23 import com.example.demo.entity.*; 4 import com.example.demo.model.MyUserDetails; 5 import com.example.demo.repository.*; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.security.core.GrantedAuthority; 8 import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 import org.springframework.security.core.userdetails.UserDetails;10 import org.springframework.security.core.userdetails.UserDetailsService;11 import org.springframework.security.core.userdetails.UsernameNotFoundException;12 import org.springframework.stereotype.Component;13 14 import java.util.ArrayList;15 import java.util.List;16 import java.util.Optional;17 import java.util.stream.Collectors;18 19 @Component20 public class MyUserDetailsService implements UserDetailsService {21 22@Autowired23private SysUserRepository sysUserRepository;24@Autowired25private SysRoleRepository sysRoleRepository;26@Autowired27private SysUserRoleRelationRepository sysUserRoleRelationRepository;28@Autowired29private SysRolePermissionRelationRepository sysRolePermissionRelationRepository;30@Autowired31private SysPermissionRepository sysPermissionRepository;32 33@Override34public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {35//查用户36Optional<SysUser> optionalSysUser = sysUserRepository.findByUsername(username);37SysUser sysUser = optionalSysUser.orElseThrow(()->new UsernameNotFoundException("用户名" + username + "不存在"));38 39//查权限40List<SysUserRoleRelation> sysUserRoleRelationList = sysUserRoleRelationRepository.findByUserId(sysUser.getId());41List<Integer> roleIds = sysUserRoleRelationList.stream().map(SysUserRoleRelation::getRoleId).collect(Collectors.toList());42List<SysRole> sysRoleList = sysRoleRepository.findByIdIn(roleIds);43List<SysRolePermissionRelation> sysRolePermissionRelationList = sysRolePermissionRelationRepository.findByRoleIdIn(roleIds);44List<Integer> permissionIds = sysRolePermissionRelationList.stream().map(SysRolePermissionRelation::getPermissionId).collect(Collectors.toList());45List<SysPermission> sysPermissionList = sysPermissionRepository.findByIdIn(permissionIds);46 47List<GrantedAuthority> grantedAuthorities = new ArrayList<>(sysPermissionList.size());48for (SysPermission permission : sysPermissionList) {49grantedAuthorities.add(new SimpleGrantedAuthority(permission.getUrl()));50}51sysRoleList.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode())));52 53MyUserDetails myUserDetails = new MyUserDetails(sysUser.getUsername(), sysUser.getPassword(), sysUser.isEnabled());54myUserDetails.setAuthorities(grantedAuthorities);55 56return myUserDetails;57}58 }这里用的JPA,相关的实体类及Repository太多就不一一贴出来了,只代表性的贴一个
SysRole.java 
1 package com.example.demo.entity; 23 import lombok.Data; 45 import javax.persistence.*; 6 import java.io.Serializable; 7 import java.time.LocalDateTime; 89 @Data10 @Entity11 @Table(name = "sys_role")12 public class SysRole implements Serializable {13 14@Id15@GeneratedValue(strategy = GenerationType.AUTO)16private Integer id;17 18private String roleName;19 20private String roleCode;21 22private String roleDesc;23 24private LocalDateTime createTime;25 26private LocalDateTime updateTime;27 }SysRoleRepository.java
1 package com.example.demo.repository; 23 import com.example.demo.entity.SysRole; 4 import org.springframework.data.jpa.repository.JpaRepository; 56 import java.util.List; 78 public interface SysRoleRepository extends JpaRepository<SysRole, Integer> { 9 10List<SysRole> findByIdIn(List<Integer> ids);11 }application.properties
1 spring.datasource.url=jdbc:mysql://localhost:3306/test2 spring.datasource.username=root3 spring.datasource.password=1234564 spring.datasource.driver-class-name=com.mysql.jdbc.Driver5 6 spring.jpa.database=mysql最后,也是最重要的是配置WebSecurity
WebSecurityConfig.java 
1 package com.example.demo.config; 23 import com.example.demo.handler.MyAuthenticationFailureHandler; 4 import com.example.demo.handler.MyAuthenticationSuccessHandler; 5 import com.example.demo.handler.MyExpiredSessionStrategy; 6 import com.example.demo.service.MyUserDetailsService; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.context.annotation.Bean; 9 import org.springframework.context.annotation.Configuration;10 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;11 import org.springframework.security.config.annotation.web.builders.HttpSecurity;12 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;13 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;14 import org.springframework.security.crypto.password.PasswordEncoder;15 16 @Configuration17 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {18 19@Autowired20private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;21@Autowired22private MyAuthenticationFailureHandler myAuthenticationFailureHandler;23@Autowired24private MyUserDetailsService myUserDetailsService;25 26@Override27protected void configure(AuthenticationManagerBuilder auth) throws Exception {28auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());29}30 31@Override32protected void configure(HttpSecurity http) throws Exception {33http.formLogin()34.loginProcessingUrl("/login")35.usernameParameter("username")36.passwordParameter("password")37.successHandler(myAuthenticationSuccessHandler)38.failureHandler(myAuthenticationFailureHandler)39.and()40.authorizeRequests()41.antMatchers("/login.html", "/login").permitAll()42.antMatchers("/hello/sayHello").hasAnyAuthority("ROLE_user", "ROLE_admin")43.antMatchers("/hello/sayHi").hasAnyRole("admin")44.anyRequest().authenticated()45.and()46.sessionManagement().sessionFixation().migrateSession()47.maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());48}49 50@Bean51public PasswordEncoder passwordEncoder() {52return new BCryptPasswordEncoder();53}54 55 }改完后的项目结构如下
Spring Security 入门篇

文章插图
 
4.  动态加载权限规则配置
鉴权规则就是判断请求的资源是不是在当前用户可访问的资源列表中
那么,首先,定义一个方法来实现这个逻辑
1 package com.example.demo.service; 23 import org.springframework.security.core.Authentication; 4 import org.springframework.security.core.authority.SimpleGrantedAuthority; 5 import org.springframework.security.core.userdetails.UserDetails; 6 import org.springframework.stereotype.Component; 78 import javax.servlet.http.HttpServletRequest; 9 10 @Component("myAccessDecisionService")11 public class MyAccessDecisionService {12 13public boolean hasPermission(HttpServletRequest request, Authentication authentication) {14Object principal = authentication.getPrincipal();15if (principal instanceof UserDetails) {16UserDetails userDetails = (UserDetails) principal;17SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(request.getRequestURI());18return userDetails.getAuthorities().contains(simpleGrantedAuthority);19}20return false;21}22 }然后,在WebSecurityConfig中配置,替换原来写死的匹配规则
1 package com.example.demo.config; 23 import com.example.demo.handler.MyAuthenticationFailureHandler; 4 import com.example.demo.handler.MyAuthenticationSuccessHandler; 5 import com.example.demo.handler.MyExpiredSessionStrategy; 6 import com.example.demo.service.MyUserDetailsService; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.context.annotation.Bean; 9 import org.springframework.context.annotation.Configuration;10 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;11 import org.springframework.security.config.annotation.web.builders.HttpSecurity;12 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;13 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;14 import org.springframework.security.crypto.password.PasswordEncoder;15 16 @Configuration17 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {18 19@Autowired20private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;21@Autowired22private MyAuthenticationFailureHandler myAuthenticationFailureHandler;23@Autowired24private MyUserDetailsService myUserDetailsService;25 26@Override27protected void configure(AuthenticationManagerBuilder auth) throws Exception {28auth.userDetailsService(myUserDetailsService)29.passwordEncoder(passwordEncoder());30}31 32@Override33protected void configure(HttpSecurity http) throws Exception {34http.formLogin()35.loginProcessingUrl("/login")36.usernameParameter("username")37.passwordParameter("password")38.successHandler(myAuthenticationSuccessHandler)39.failureHandler(myAuthenticationFailureHandler)40.and()41.authorizeRequests()42.antMatchers("/login.html", "/login").permitAll()43.anyRequest().access("@myAccessDecisionService.hasPermission(request, authentication)")44.and()45.sessionManagement().sessionFixation().migrateSession()46.maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());47}48 49@Bean50public PasswordEncoder passwordEncoder() {51return new BCryptPasswordEncoder();52}53 54 }改造后的项目结构如下
Spring Security 入门篇

文章插图
 
关于权限(资源)访问规则,还有一种写法,这种方式是我在网上看到的,就是利用 FilterInvocationSecurityMetadataSource 和 AccessDecisionManager
这里我稍微改了一下,先来创建两个实现类
首先是MyFilterInvocationSecurityMetadataSource.java
1 package com.example.demo.service; 23 import com.example.demo.entity.SysPermission; 4 import com.example.demo.repository.SysPermissionRepository; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.security.access.ConfigAttribute; 7 import org.springframework.security.access.SecurityConfig; 8 import org.springframework.security.web.FilterInvocation; 9 import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;10 import org.springframework.stereotype.Component;11 import org.springframework.util.AntPathMatcher;12 import org.springframework.util.CollectionUtils;13 14 import java.util.Collection;15 import java.util.List;16 import java.util.stream.Collectors;17 18 @Component19 public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {20 21private AntPathMatcher pathMatcher = new AntPathMatcher();22 23@Autowired24private SysPermissionRepository sysPermissionRepository;25 26@Override27public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {28String requestUrl = ((FilterInvocation) object).getRequestUrl();29 30//查找与当前请求URL匹配的所有权限31List<SysPermission> sysPermissionList = sysPermissionRepository.findAll();32List<String> urls = sysPermissionList.stream()33.map(SysPermission::getUrl)34.filter(e->pathMatcher.match(e, requestUrl))35.distinct()36.collect(Collectors.toList());37 38if (!CollectionUtils.isEmpty(urls)) {39return SecurityConfig.createList(urls.toArray(new String[urls.size()]));40}41 42return SecurityConfig.createList("ROLE_login");43}44 45@Override46public Collection<ConfigAttribute> getAllConfigAttributes() {47return null;48}49 50@Override51public boolean supports(Class<?> clazz) {52return true;53}54 }MyAccessDecisionManager.java 
1 package com.example.demo.service; 23 import org.springframework.security.access.AccessDecisionManager; 4 import org.springframework.security.access.AccessDeniedException; 5 import org.springframework.security.access.ConfigAttribute; 6 import org.springframework.security.authentication.AnonymousAuthenticationToken; 7 import org.springframework.security.authentication.InsufficientAuthenticationException; 8 import org.springframework.security.core.Authentication; 9 import org.springframework.security.core.GrantedAuthority;10 import org.springframework.security.web.FilterInvocation;11 import org.springframework.stereotype.Component;12 13 import java.util.Collection;14 import java.util.List;15 import java.util.stream.Collectors;16 17 @Component18 public class MyAccessDecisionManager implements AccessDecisionManager {19 20/**21*22* @param authentication当前登录用户,可以获取用户的权限列表23* @param objectFilterInvocation对象,可以获取请求url24* @param configAttributes25* @throws AccessDeniedException26* @throws InsufficientAuthenticationException27*/28@Override29public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {30String requestUrl = ((FilterInvocation) object).getRequestUrl();31System.out.println(requestUrl);32 33//当前用户拥有的权限(能访问的资源)34Collection<? extends GrantedAuthority> grantedAuthorities = authentication.getAuthorities();35List<String> authorities = grantedAuthorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());36 37/*if (!authorities.contains(requestUrl)) {38throw new AccessDeniedException("权限不足");39}*/40 41//判断访问当前资源所需要的权限用户是否拥有42//PS: 在我看来,其实就是看两个集合是否有交集43 44for (ConfigAttribute configAttribute : configAttributes) {45String attr = configAttribute.getAttribute();46if ("ROLE_login".equals(attr)) {47if (authentication instanceof AnonymousAuthenticationToken) {48throw new AccessDeniedException("非法请求");49}50}51 52if (authorities.contains(attr)) {53return;54}55}56 57throw new AccessDeniedException("权限不足");58}59 60@Override61public boolean supports(ConfigAttribute attribute) {62return true;63}64 65@Override66public boolean supports(Class<?> clazz) {67return true;68}69 }最后是WebSecurityConfig
1 package com.example.demo.config; 23 import com.example.demo.handler.MyAccessDeniedHandler; 4 import com.example.demo.handler.MyAuthenticationFailureHandler; 5 import com.example.demo.handler.MyAuthenticationSuccessHandler; 6 import com.example.demo.handler.MyExpiredSessionStrategy; 7 import com.example.demo.service.MyAccessDecisionManager; 8 import com.example.demo.service.MyFilterInvocationSecurityMetadataSource; 9 import com.example.demo.service.MyUserDetailsService;10 import org.springframework.beans.factory.annotation.Autowired;11 import org.springframework.context.annotation.Bean;12 import org.springframework.context.annotation.Configuration;13 import org.springframework.security.config.annotation.ObjectPostProcessor;14 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;15 import org.springframework.security.config.annotation.web.builders.HttpSecurity;16 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;17 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;18 import org.springframework.security.crypto.password.PasswordEncoder;19 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;20 21 @Configuration22 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {23 24@Autowired25private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;26@Autowired27private MyAuthenticationFailureHandler myAuthenticationFailureHandler;28@Autowired29private MyAccessDeniedHandler myAccessDeniedHandler;30@Autowired31private MyUserDetailsService myUserDetailsService;32@Autowired33private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;34@Autowired35private MyAccessDecisionManager myAccessDecisionManager;36 37@Override38protected void configure(AuthenticationManagerBuilder auth) throws Exception {39auth.userDetailsService(myUserDetailsService)40.passwordEncoder(passwordEncoder());41}42 43@Override44protected void configure(HttpSecurity http) throws Exception {45http.formLogin()46.loginProcessingUrl("/login")47.usernameParameter("username")48.passwordParameter("password")49.defaultSuccessUrl("/")50.successHandler(myAuthenticationSuccessHandler)51.failureHandler(myAuthenticationFailureHandler)52.and()53.authorizeRequests()54.antMatchers("/login.html", "/login").permitAll()55.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {56@Override57public <O extends FilterSecurityInterceptor> O postProcess(O object) {58object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);59object.setAccessDecisionManager(myAccessDecisionManager);60return object;61}62})63.and()64.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler)65.and()66.sessionManagement().sessionFixation().migrateSession()67.maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());68}69 70@Bean71public PasswordEncoder passwordEncoder() {72return new BCryptPasswordEncoder();73}74 75 }
Spring Security 入门篇

文章插图
可以看到,FilterInvocationSecurityMetadataSource的作用就是查找当前请求的资源所对应权限,然后将所需的访问权限列表传给AccessDecisionManager;MyAccessDecisionManager的作用是判断用户是否有权限访问,判断的依据就是当前资源所对应的权限是否在用户所拥有的权限列表中 。
在我看来,就是判断两个集合是否有交集,有交集就有权限访问,否则没有权限访问
Spring Security 入门篇

文章插图
而且,这种方式的权限在表设计上应该是分了url和权限编码的,也就是说权限标识符是code,不是url 。首先,用请求url去匹配权限表,找到与之匹配的权限code,后续所有的权限比较都是比较的权限code 。这样其实也挺好 。
Spring Security 入门篇

文章插图
还有一点,注意到com.example.demo.service.MyAccessDecisionManager#decide()方法有三个参数,第一个参数代表当前登录用户,第二个参数代表用户请求,第三个参数代表访问资源所需的权限 。
本例中,用的是第一和第三个参数
但是,我觉得可以直接用第一和第二个参数,用户请求也能拿到,用户权限也能拿到,有这些就可以判断用户是否有权限了,这样的话只需要AccessDecisionManager,而不需要FilterInvocationSecurityMetadataSource了
Spring Security 入门篇

文章插图
这里补充两点:
1、这里说的权限和资源是一个意思
2、关于资源访问控制,有两种写法 。一种是基于权限编码的匹配,另一种是基于url的匹配 。
  • 第一种写法是,基于权限编码 。即在代码中定义好访问某个资源需要什么样的权限,这里需要用到@PreAuthorize注解 。
  • 第二种写法是,基于请求URL 。即数据库中配置好资源访问的URL,根据请求URL是否与之匹配来判断 。(PS:可以比较权限编码,也可以比较权限URL) 
5.  退出登录
1 package com.example.demo.config; 23 import com.example.demo.handler.*; 4 import com.example.demo.service.MyAccessDecisionManager; 5 import com.example.demo.service.MyFilterInvocationSecurityMetadataSource; 6 import com.example.demo.service.MyUserDetailsService; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.context.annotation.Bean; 9 import org.springframework.context.annotation.Configuration;10 import org.springframework.security.config.annotation.ObjectPostProcessor;11 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;12 import org.springframework.security.config.annotation.web.builders.HttpSecurity;13 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;14 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;15 import org.springframework.security.crypto.password.PasswordEncoder;16 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;17 18 @Configuration19 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {20 21@Autowired22private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;23@Autowired24private MyAuthenticationFailureHandler myAuthenticationFailureHandler;25@Autowired26private MyAccessDeniedHandler myAccessDeniedHandler;27@Autowired28private MyLogoutSuccessHandler myLogoutSuccessHandler;29@Autowired30private MyUserDetailsService myUserDetailsService;31@Autowired32private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;33@Autowired34private MyAccessDecisionManager myAccessDecisionManager;35 36@Override37protected void configure(AuthenticationManagerBuilder auth) throws Exception {38auth.userDetailsService(myUserDetailsService)39.passwordEncoder(passwordEncoder());40}41 42@Override43protected void configure(HttpSecurity http) throws Exception {44http.formLogin()45.loginProcessingUrl("/login")46.usernameParameter("username")47.passwordParameter("password")48.defaultSuccessUrl("/")49.successHandler(myAuthenticationSuccessHandler)50.failureHandler(myAuthenticationFailureHandler)51.and().logout()52.logoutUrl("/logout")53.logoutSuccessHandler(myLogoutSuccessHandler)54.and()55.authorizeRequests()56.antMatchers("/login.html", "/login").permitAll()57.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {58@Override59public <O extends FilterSecurityInterceptor> O postProcess(O object) {60object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);61object.setAccessDecisionManager(myAccessDecisionManager);62return object;63}64})65.and()66.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler)67.and()68.sessionManagement().sessionFixation().migrateSession()69.maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());70}71 72@Bean73public PasswordEncoder passwordEncoder() {74return new BCryptPasswordEncoder();75}76 77 }自定义LogoutSuccessHandler
1 package com.example.demo.handler; 23 import com.fasterxml.jackson.databind.ObjectMapper; 4 import org.springframework.security.core.Authentication; 5 import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 6 import org.springframework.stereotype.Component; 78 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServletRequest;10 import javax.servlet.http.HttpServletResponse;11 import java.io.IOException;12 import java.io.PrintWriter;13 14 @Component15 public class MyLogoutSuccessHandler implements LogoutSuccessHandler {16 17private static ObjectMapper objectMapper = new ObjectMapper();18 19@Override20public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {21 //response.sendRedirect("/login.html");22 23response.setContentType("application/json;charset=utf-8");24PrintWriter printWriter = response.getWriter();25printWriter.write(objectMapper.writeValueAsString("logout success"));26printWriter.flush();27printWriter.close();28}29 }到这里为止,我们已经实现了用户动态加载,权限匹配规则动态加载,即谁可以访问什么资源这个过程已经不再是写死了,而是全部可配置化了
6. 集成JWT生成token
现在的项目都是前后端分离的,客户端与服务端通过接口进行交互,数据格式采用JSON,这就要求服务端是无状态的 。如果还是利用Session在服务端维持会话的话,可扩展性就太差了 。总之一句话,用Session就是有状态的,用Token就是无状态的,因此,我们要用Token来识别用户身份 。
默认会话是Session维持的,用Session的话不利于水平扩容(尽管共享Session,但还是很不方便),而且也没法做前后端分离 。因此,需要用token来承载认证用户信息,前后端通过json进行交互 。
首先,引入依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>然后,JWT工具类
1 package com.example.demo.util; 23 import io.jsonwebtoken.*; 45 import java.util.Date; 6 import java.util.HashMap; 7 import java.util.Map; 8 import java.util.function.Function; 9 10 /**11* @Author ChengJianSheng12* @Date 2021/5/713*/14 public class JwtUtil {15 16private static long TOKEN_EXPIRATION = 24 * 60 * 60 * 1000;17private static String TOKEN_SECRET_KEY = "123456";18 19/**20* 生成Token21* @param subject用户名22* @return23*/24public static String createToken(String subject) {25long currentTimeMillis = System.currentTimeMillis();26Date currentDate = new Date(currentTimeMillis);27Date expirationDate = new Date(currentTimeMillis + TOKEN_EXPIRATION);28 29//存放自定义属性,比如用户拥有的权限30Map<String, Object> claims = new HashMap<>();31 32return Jwts.builder()33.setClaims(claims)34.setSubject(subject)35.setIssuedAt(currentDate)36.setExpiration(expirationDate)37.signWith(SignatureAlgorithm.HS512, TOKEN_SECRET_KEY)38.compact();39}40 41public static String extractUsername(String token) {42return extractClaim(token, Claims::getSubject);43}44 45public static boolean isTokenExpired(String token) {46return extractExpiration(token).before(new Date());47}48 49public static Date extractExpiration(String token) {50return extractClaim(token, Claims::getExpiration);51}52 53public static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {54final Claims claims = extractAllClaims(token);55return claimsResolver.apply(claims);56}57 58private static Claims extractAllClaims(String token) {59return Jwts.parser().setSigningKey(TOKEN_SECRET_KEY).parseClaimsJws(token).getBody();60}61 62 }登录成功后,将token返回给客户端
1 package com.example.demo.handler; 23 import com.example.demo.model.MyUserDetails; 4 import com.example.demo.util.JwtUtil; 5 import com.fasterxml.jackson.databind.ObjectMapper; 6 import org.springframework.security.core.Authentication; 7 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 8 import org.springframework.stereotype.Component; 9 10 import javax.servlet.ServletException;11 import javax.servlet.http.HttpServletRequest;12 import javax.servlet.http.HttpServletResponse;13 import java.io.IOException;14 15 @Component16 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {17 18private static ObjectMapper objectMapper = new ObjectMapper();19 20@Override21public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {22MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();23String username = myUserDetails.getUsername();24String token = JwtUtil.createToken(username);25//todo 缓存到 Redis26//todo 把token存到Redis中27 28response.setContentType("application/json;charset=utf-8");29response.getWriter().write(objectMapper.writeValueAsString(token));30}31 }每次请求过来,从token中取到用户信息,然后放到上下文中
1 package com.example.demo.filter; 23 import com.example.demo.service.MyUserDetailsService; 4 import com.example.demo.util.JwtUtil; 5 import org.apache.commons.lang3.StringUtils; 6 import org.springframework.security.authentication.AuthenticationManager; 7 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 import org.springframework.security.core.context.SecurityContextHolder; 9 import org.springframework.security.core.userdetails.UserDetails;10 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;11 import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;12 13 import javax.servlet.FilterChain;14 import javax.servlet.ServletException;15 import javax.servlet.http.HttpServletRequest;16 import javax.servlet.http.HttpServletResponse;17 import java.io.IOException;18 19 /**20* 负责在每次请求中,解析请求头中的token,从中取得用户信息,生成认证对象传递给下一个过滤器21* @Author ChengJianSheng22* @Date 2021/5/723*/24 public class JwtAuthenticationFilter extends BasicAuthenticationFilter {25 26private MyUserDetailsService myUserDetailsService;27 28public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {29super(authenticationManager);30}31 32public JwtAuthenticationFilter(AuthenticationManager authenticationManager, MyUserDetailsService myUserDetailsService) {33super(authenticationManager);34this.myUserDetailsService = myUserDetailsService;35}36 37@Override38protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {39String token = request.getHeader("token");40System.out.println("请求头中带的token: " + token);41if (StringUtils.isNoneBlank(token)) {42if (!JwtUtil.isTokenExpired(token)) {43String username = JwtUtil.extractUsername(token);44if (StringUtils.isNoneBlank(username) && null == SecurityContextHolder.getContext().getAuthentication()) {45//查询用户权限,有以下三种方式:46//1. 可以从数据库中加载47//2. 可以从Redis中加载(PS: 前提是之前已经缓存到Redis中了)48//3. 可以从token中加载(PS: 前提是生成token的时候把用户权限作为Claims放置其中了)49 50UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);51 52UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());53authRequest.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));54 55SecurityContextHolder.getContext().setAuthentication(authRequest);56}57}58}59 60chain.doFilter(request, response);61}62 }把这个过滤器添加到
1 http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager(), myUserDetailsService), UsernamePasswordAuthenticationFilter.class);完整配置如下:
1 package com.example.demo.config; 23 import com.example.demo.filter.JwtAuthenticationFilter; 4 import com.example.demo.handler.*; 5 import com.example.demo.service.MyAccessDecisionManager; 6 import com.example.demo.service.MyFilterInvocationSecurityMetadataSource; 7 import com.example.demo.service.MyUserDetailsService; 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.context.annotation.Bean;10 import org.springframework.context.annotation.Configuration;11 import org.springframework.security.config.annotation.ObjectPostProcessor;12 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;13 import org.springframework.security.config.annotation.web.builders.HttpSecurity;14 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;15 import org.springframework.security.config.http.SessionCreationPolicy;16 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;17 import org.springframework.security.crypto.password.PasswordEncoder;18 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;19 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;20 21 @Configuration22 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {23 24@Autowired25private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;26@Autowired27private MyAuthenticationFailureHandler myAuthenticationFailureHandler;28@Autowired29private MyAccessDeniedHandler myAccessDeniedHandler;30@Autowired31private MyLogoutSuccessHandler myLogoutSuccessHandler;32@Autowired33private MyUserDetailsService myUserDetailsService;34@Autowired35private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;36@Autowired37private MyAccessDecisionManager myAccessDecisionManager;38@Autowired39private MyAuthenticationEntryPoint myAuthenticationEntryPoint;40 41@Override42protected void configure(AuthenticationManagerBuilder auth) throws Exception {43auth.userDetailsService(myUserDetailsService)44.passwordEncoder(passwordEncoder());45}46 47@Override48protected void configure(HttpSecurity http) throws Exception {49http.formLogin()50.loginProcessingUrl("/login")51.usernameParameter("username")52.passwordParameter("password")53.successHandler(myAuthenticationSuccessHandler)54.failureHandler(myAuthenticationFailureHandler)55.and().logout()56.logoutUrl("/logout")57.logoutSuccessUrl("/login.html")58.logoutSuccessHandler(myLogoutSuccessHandler)59.and()60.authorizeRequests()61.antMatchers("/login.html", "/login").permitAll()62.anyRequest().access("@myAccessDecisionService.hasPermission(request, authentication)")63.and()64.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler).authenticationEntryPoint(myAuthenticationEntryPoint)65.and()66.sessionManagement().sessionFixation().migrateSession().sessionCreationPolicy(SessionCreationPolicy.STATELESS)67.maximumSessions(1).maxSessionsPreventsLogin(false).expiredSessionStrategy(new MyExpiredSessionStrategy());68 69http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager(), myUserDetailsService), UsernamePasswordAuthenticationFilter.class);70 71http.csrf().disable();72}73 74@Bean75public PasswordEncoder passwordEncoder() {76return new BCryptPasswordEncoder();77}78 79 }增加一个未登录的处理
1 package com.example.demo.handler; 23 import com.fasterxml.jackson.databind.ObjectMapper; 4 import org.springframework.security.core.AuthenticationException; 5 import org.springframework.security.web.AuthenticationEntryPoint; 6 import org.springframework.stereotype.Component; 78 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServletRequest;10 import javax.servlet.http.HttpServletResponse;11 import java.io.IOException;12 13 /**14* 未认证(未登录)统一处理15* @Author ChengJianSheng16* @Date 2021/5/717*/18 @Component19 public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {20 21private static ObjectMapper objectMapper = new ObjectMapper();22 23@Override24public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {25response.setContentType("application/json;charset=utf-8");26response.getWriter().write(objectMapper.writeValueAsString("未登录,请先登录"));27}28 }改造后的项目结构如下
Spring Security 入门篇

文章插图
最后,用token以后,退出要做一点改动 。由于我们采用JWT来生成Token,因此token是没法撤销和删除的,所以此时的退出应该是:
  1. Token生成以后要保存到数据库(MySQL或者Redis)
  2. 每次请求要校验Token是否存在及有效
  3. 退出登录后删除数据库中保存的Token
【Spring Security 入门篇】关于Spring Security实现简单的用户、角色、权限控制就先讲到这里,稍微做一个回顾:
  1. 未认证(登录)的用户提示他要先登录
  2. 已认证的用户判断是否有权限访问