本文是一个笔记系列,目标是完成一个基于角色的权限访问控制系统(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;
2
3 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 @Configuration
12 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
13
14 @Override
15 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
16 auth.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 @Override
25 protected void configure(HttpSecurity http) throws Exception {
26 http.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 @Bean
41 public PasswordEncoder passwordEncoder() {
42 return new BCryptPasswordEncoder();
43 }
44 }

HelloController.java

 1 package com.example.demo.controller;
2
3 import org.springframework.web.bind.annotation.GetMapping;
4 import org.springframework.web.bind.annotation.RequestMapping;
5 import org.springframework.web.bind.annotation.RestController;
6
7 @RestController
8 @RequestMapping("/hello")
9 public class HelloController {
10
11 @GetMapping("/sayHello")
12 public String sayHello() {
13 return "hello";
14 }
15
16 @GetMapping("/sayHi")
17 public String sayHi() {
18 return "hi";
19 }
20
21 }

项目结构

定义了两个用户zhangsan和admin,他们的密码都是123456,zhangsan的角色是user可以访问/hello/sayHello,admin的角色是admin可以访问/hello/sayHello和hello/sayHi

2.  认证成功/失败处理

按照刚才的写法,登录成功之后是跳到/页面,失败跳转到登录页。但是,对于前后端分离的项目,我希望它返回json数据,而不是重定向到某个页面

处理用户名和密码登录的过滤器是org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter,既然是过滤器,直接看doFilter方法

不用多说,自定义认证成功处理器

 1 package com.example.demo.handler;
2
3 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;
7
8 import javax.servlet.ServletException;
9 import javax.servlet.http.HttpServletRequest;
10 import javax.servlet.http.HttpServletResponse;
11 import java.io.IOException;
12
13 @Component
14 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
15
16 private static ObjectMapper objectMapper = new ObjectMapper();
17
18 @Override
19 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
20 response.setContentType("application/json;charset=utf-8");
21 response.getWriter().write(objectMapper.writeValueAsString("ok"));
22 }
23 }

自定义认证失败处理器

 1 package com.example.demo.handler;
2
3 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;
7
8 import javax.servlet.ServletException;
9 import javax.servlet.http.HttpServletRequest;
10 import javax.servlet.http.HttpServletResponse;
11 import java.io.IOException;
12
13 @Component
14 public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
15
16 private static ObjectMapper objectMapper = new ObjectMapper();
17
18 @Override
19 public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
20 response.setContentType("application/json;charset=utf-8");
21 response.getWriter().write(objectMapper.writeValueAsString("error"));
22 }
23 }

WebSecurityConfig配置

 1 package com.example.demo.config;
2
3 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 @Configuration
16 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
17
18 @Autowired
19 private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
20 @Autowired
21 private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
22
23 @Override
24 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
25 auth.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 @Override
34 protected void configure(HttpSecurity http) throws Exception {
35 http.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 @Bean
55 public PasswordEncoder passwordEncoder() {
56 return new BCryptPasswordEncoder();
57 }
58 }

再多自定义一个Session过期策略,当Session过期或者被踢下线以后的处理逻辑

 1 package com.example.demo.handler;
2
3 import com.fasterxml.jackson.databind.ObjectMapper;
4 import org.springframework.security.web.session.SessionInformationExpiredEvent;
5 import org.springframework.security.web.session.SessionInformationExpiredStrategy;
6
7 import javax.servlet.ServletException;
8 import javax.servlet.http.HttpServletResponse;
9 import java.io.IOException;
10
11 public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {
12
13 private static ObjectMapper objectMapper = new ObjectMapper();
14
15 @Override
16 public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
17 String msg = "登录超时或已在另一台机器登录,您被迫下线!";
18 HttpServletResponse response = event.getResponse();
19 response.setContentType("application/json;charset=utf-8");
20 response.getWriter().write(objectMapper.writeValueAsString(msg));
21 }
22 }

3.  从数据库中加载用户及权限

刚才用户是在内存中定义的,这肯定是不行的,下面从数据库中加载用户及其所拥有的权限

最简单的结构是这样的:

为了减少用户的重复授权,引入用户组。将用户加入用户组以后,就自动拥有组所对应的权限。

下面,按照最简单的用户角色权限模型来改造刚才的项目

首先,通过实现UserDetails接口来自定义一个用户信息对象

MyUserDetails.java

 1 package com.example.demo.model;
2
3 import org.springframework.security.core.GrantedAuthority;
4 import org.springframework.security.core.userdetails.UserDetails;
5
6 import java.util.Collection;
7
8 public class MyUserDetails implements UserDetails {
9
10 private String username;
11 private String password;
12 private boolean enabled;
13 private Collection<? extends GrantedAuthority> authorities;
14
15 public MyUserDetails(String username, String password, boolean enabled) {
16 this.username = username;
17 this.password = password;
18 this.enabled = enabled;
19 }
20
21 public void setUsername(String username) {
22 this.username = username;
23 }
24
25 public void setPassword(String password) {
26 this.password = password;
27 }
28
29 public void setEnabled(boolean enabled) {
30 this.enabled = enabled;
31 }
32
33 public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
34 this.authorities = authorities;
35 }
36
37 @Override
38 public Collection<? extends GrantedAuthority> getAuthorities() {
39 return authorities;
40 }
41
42 @Override
43 public String getPassword() {
44 return password;
45 }
46
47 @Override
48 public String getUsername() {
49 return username;
50 }
51
52 @Override
53 public boolean isAccountNonExpired() {
54 return true;
55 }
56
57 @Override
58 public boolean isAccountNonLocked() {
59 return true;
60 }
61
62 @Override
63 public boolean isCredentialsNonExpired() {
64 return true;
65 }
66
67 @Override
68 public boolean isEnabled() {
69 return enabled;
70 }
71 }

有了UserDetails以后,还需要UserDetailsService去加载它,所以自定义一个UserDetailsService

MyUserDetailsService.java

 1 package com.example.demo.service;
2
3 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 @Component
20 public class MyUserDetailsService implements UserDetailsService {
21
22 @Autowired
23 private SysUserRepository sysUserRepository;
24 @Autowired
25 private SysRoleRepository sysRoleRepository;
26 @Autowired
27 private SysUserRoleRelationRepository sysUserRoleRelationRepository;
28 @Autowired
29 private SysRolePermissionRelationRepository sysRolePermissionRelationRepository;
30 @Autowired
31 private SysPermissionRepository sysPermissionRepository;
32
33 @Override
34 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
35 // 查用户
36 Optional<SysUser> optionalSysUser = sysUserRepository.findByUsername(username);
37 SysUser sysUser = optionalSysUser.orElseThrow(()->new UsernameNotFoundException("用户名" + username + "不存在"));
38
39 // 查权限
40 List<SysUserRoleRelation> sysUserRoleRelationList = sysUserRoleRelationRepository.findByUserId(sysUser.getId());
41 List<Integer> roleIds = sysUserRoleRelationList.stream().map(SysUserRoleRelation::getRoleId).collect(Collectors.toList());
42 List<SysRole> sysRoleList = sysRoleRepository.findByIdIn(roleIds);
43 List<SysRolePermissionRelation> sysRolePermissionRelationList = sysRolePermissionRelationRepository.findByRoleIdIn(roleIds);
44 List<Integer> permissionIds = sysRolePermissionRelationList.stream().map(SysRolePermissionRelation::getPermissionId).collect(Collectors.toList());
45 List<SysPermission> sysPermissionList = sysPermissionRepository.findByIdIn(permissionIds);
46
47 List<GrantedAuthority> grantedAuthorities = new ArrayList<>(sysPermissionList.size());
48 for (SysPermission permission : sysPermissionList) {
49 grantedAuthorities.add(new SimpleGrantedAuthority(permission.getUrl()));
50 }
51 sysRoleList.forEach(role->grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode())));
52
53 MyUserDetails myUserDetails = new MyUserDetails(sysUser.getUsername(), sysUser.getPassword(), sysUser.isEnabled());
54 myUserDetails.setAuthorities(grantedAuthorities);
55
56 return myUserDetails;
57 }
58 }

这里用的JPA,相关的实体类及Repository太多就不一一贴出来了,只代表性的贴一个

SysRole.java

 1 package com.example.demo.entity;
2
3 import lombok.Data;
4
5 import javax.persistence.*;
6 import java.io.Serializable;
7 import java.time.LocalDateTime;
8
9 @Data
10 @Entity
11 @Table(name = "sys_role")
12 public class SysRole implements Serializable {
13
14 @Id
15 @GeneratedValue(strategy = GenerationType.AUTO)
16 private Integer id;
17
18 private String roleName;
19
20 private String roleCode;
21
22 private String roleDesc;
23
24 private LocalDateTime createTime;
25
26 private LocalDateTime updateTime;
27 }

SysRoleRepository.java

 1 package com.example.demo.repository;
2
3 import com.example.demo.entity.SysRole;
4 import org.springframework.data.jpa.repository.JpaRepository;
5
6 import java.util.List;
7
8 public interface SysRoleRepository extends JpaRepository<SysRole, Integer> {
9
10 List<SysRole> findByIdIn(List<Integer> ids);
11 }

application.properties

1 spring.datasource.url=jdbc:mysql://localhost:3306/test
2 spring.datasource.username=root
3 spring.datasource.password=123456
4 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
5
6 spring.jpa.database=mysql

最后,也是最重要的是配置WebSecurity

WebSecurityConfig.java

 1 package com.example.demo.config;
2
3 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 @Configuration
17 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
18
19 @Autowired
20 private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
21 @Autowired
22 private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
23 @Autowired
24 private MyUserDetailsService myUserDetailsService;
25
26 @Override
27 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
28 auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
29 }
30
31 @Override
32 protected void configure(HttpSecurity http) throws Exception {
33 http.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 @Bean
51 public PasswordEncoder passwordEncoder() {
52 return new BCryptPasswordEncoder();
53 }
54
55 }

改完后的项目结构如下

4.  动态加载权限规则配置

鉴权规则就是判断请求的资源是不是在当前用户可访问的资源列表中

那么,首先,定义一个方法来实现这个逻辑

 1 package com.example.demo.service;
2
3 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;
7
8 import javax.servlet.http.HttpServletRequest;
9
10 @Component("myAccessDecisionService")
11 public class MyAccessDecisionService {
12
13 public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
14 Object principal = authentication.getPrincipal();
15 if (principal instanceof UserDetails) {
16 UserDetails userDetails = (UserDetails) principal;
17 SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(request.getRequestURI());
18 return userDetails.getAuthorities().contains(simpleGrantedAuthority);
19 }
20 return false;
21 }
22 }

然后,在WebSecurityConfig中配置,替换原来写死的匹配规则

 1 package com.example.demo.config;
2
3 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 @Configuration
17 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
18
19 @Autowired
20 private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
21 @Autowired
22 private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
23 @Autowired
24 private MyUserDetailsService myUserDetailsService;
25
26 @Override
27 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
28 auth.userDetailsService(myUserDetailsService)
29 .passwordEncoder(passwordEncoder());
30 }
31
32 @Override
33 protected void configure(HttpSecurity http) throws Exception {
34 http.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 @Bean
50 public PasswordEncoder passwordEncoder() {
51 return new BCryptPasswordEncoder();
52 }
53
54 }

改造后的项目结构如下

关于权限(资源)访问规则,还有一种写法,这种方式是我在网上看到的,就是利用 FilterInvocationSecurityMetadataSource 和 AccessDecisionManager

这里我稍微改了一下,先来创建两个实现类

首先是MyFilterInvocationSecurityMetadataSource.java

 1 package com.example.demo.service;
2
3 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 @Component
19 public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
20
21 private AntPathMatcher pathMatcher = new AntPathMatcher();
22
23 @Autowired
24 private SysPermissionRepository sysPermissionRepository;
25
26 @Override
27 public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
28 String requestUrl = ((FilterInvocation) object).getRequestUrl();
29
30 // 查找与当前请求URL匹配的所有权限
31 List<SysPermission> sysPermissionList = sysPermissionRepository.findAll();
32 List<String> urls = sysPermissionList.stream()
33 .map(SysPermission::getUrl)
34 .filter(e->pathMatcher.match(e, requestUrl))
35 .distinct()
36 .collect(Collectors.toList());
37
38 if (!CollectionUtils.isEmpty(urls)) {
39 return SecurityConfig.createList(urls.toArray(new String[urls.size()]));
40 }
41
42 return SecurityConfig.createList("ROLE_login");
43 }
44
45 @Override
46 public Collection<ConfigAttribute> getAllConfigAttributes() {
47 return null;
48 }
49
50 @Override
51 public boolean supports(Class<?> clazz) {
52 return true;
53 }
54 }

MyAccessDecisionManager.java

 1 package com.example.demo.service;
2
3 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 @Component
18 public class MyAccessDecisionManager implements AccessDecisionManager {
19
20 /**
21 *
22 * @param authentication 当前登录用户,可以获取用户的权限列表
23 * @param object FilterInvocation对象,可以获取请求url
24 * @param configAttributes
25 * @throws AccessDeniedException
26 * @throws InsufficientAuthenticationException
27 */
28 @Override
29 public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
30 String requestUrl = ((FilterInvocation) object).getRequestUrl();
31 System.out.println(requestUrl);
32
33 // 当前用户拥有的权限(能访问的资源)
34 Collection<? extends GrantedAuthority> grantedAuthorities = authentication.getAuthorities();
35 List<String> authorities = grantedAuthorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
36
37 /*if (!authorities.contains(requestUrl)) {
38 throw new AccessDeniedException("权限不足");
39 }*/
40
41 // 判断访问当前资源所需要的权限用户是否拥有
42 // PS: 在我看来,其实就是看两个集合是否有交集
43
44 for (ConfigAttribute configAttribute : configAttributes) {
45 String attr = configAttribute.getAttribute();
46 if ("ROLE_login".equals(attr)) {
47 if (authentication instanceof AnonymousAuthenticationToken) {
48 throw new AccessDeniedException("非法请求");
49 }
50 }
51
52 if (authorities.contains(attr)) {
53 return;
54 }
55 }
56
57 throw new AccessDeniedException("权限不足");
58 }
59
60 @Override
61 public boolean supports(ConfigAttribute attribute) {
62 return true;
63 }
64
65 @Override
66 public boolean supports(Class<?> clazz) {
67 return true;
68 }
69 }

最后是WebSecurityConfig

 1 package com.example.demo.config;
2
3 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 @Configuration
22 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
23
24 @Autowired
25 private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
26 @Autowired
27 private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
28 @Autowired
29 private MyAccessDeniedHandler myAccessDeniedHandler;
30 @Autowired
31 private MyUserDetailsService myUserDetailsService;
32 @Autowired
33 private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
34 @Autowired
35 private MyAccessDecisionManager myAccessDecisionManager;
36
37 @Override
38 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
39 auth.userDetailsService(myUserDetailsService)
40 .passwordEncoder(passwordEncoder());
41 }
42
43 @Override
44 protected void configure(HttpSecurity http) throws Exception {
45 http.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 @Override
57 public <O extends FilterSecurityInterceptor> O postProcess(O object) {
58 object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
59 object.setAccessDecisionManager(myAccessDecisionManager);
60 return 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 @Bean
71 public PasswordEncoder passwordEncoder() {
72 return new BCryptPasswordEncoder();
73 }
74
75 }

可以看到,FilterInvocationSecurityMetadataSource的作用就是查找当前请求的资源所对应权限,然后将所需的访问权限列表传给AccessDecisionManager;MyAccessDecisionManager的作用是判断用户是否有权限访问,判断的依据就是当前资源所对应的权限是否在用户所拥有的权限列表中。

在我看来,就是判断两个集合是否有交集,有交集就有权限访问,否则没有权限访问

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

还有一点,注意到com.example.demo.service.MyAccessDecisionManager#decide()方法有三个参数,第一个参数代表当前登录用户,第二个参数代表用户请求,第三个参数代表访问资源所需的权限。

本例中,用的是第一和第三个参数

但是,我觉得可以直接用第一和第二个参数,用户请求也能拿到,用户权限也能拿到,有这些就可以判断用户是否有权限了,这样的话只需要AccessDecisionManager,而不需要FilterInvocationSecurityMetadataSource了

这里补充两点:

1、这里说的权限和资源是一个意思

2、关于资源访问控制,有两种写法。一种是基于权限编码的匹配,另一种是基于url的匹配。

  • 第一种写法是,基于权限编码。即在代码中定义好访问某个资源需要什么样的权限,这里需要用到@PreAuthorize注解。
  • 第二种写法是,基于请求URL。即数据库中配置好资源访问的URL,根据请求URL是否与之匹配来判断。(PS:可以比较权限编码,也可以比较权限URL)

5.  退出登录

 1 package com.example.demo.config;
2
3 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 @Configuration
19 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
20
21 @Autowired
22 private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
23 @Autowired
24 private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
25 @Autowired
26 private MyAccessDeniedHandler myAccessDeniedHandler;
27 @Autowired
28 private MyLogoutSuccessHandler myLogoutSuccessHandler;
29 @Autowired
30 private MyUserDetailsService myUserDetailsService;
31 @Autowired
32 private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
33 @Autowired
34 private MyAccessDecisionManager myAccessDecisionManager;
35
36 @Override
37 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
38 auth.userDetailsService(myUserDetailsService)
39 .passwordEncoder(passwordEncoder());
40 }
41
42 @Override
43 protected void configure(HttpSecurity http) throws Exception {
44 http.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 @Override
59 public <O extends FilterSecurityInterceptor> O postProcess(O object) {
60 object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
61 object.setAccessDecisionManager(myAccessDecisionManager);
62 return 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 @Bean
73 public PasswordEncoder passwordEncoder() {
74 return new BCryptPasswordEncoder();
75 }
76
77 }

自定义LogoutSuccessHandler

 1 package com.example.demo.handler;
2
3 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;
7
8 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 @Component
15 public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
16
17 private static ObjectMapper objectMapper = new ObjectMapper();
18
19 @Override
20 public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
21 // response.sendRedirect("/login.html");
22
23 response.setContentType("application/json;charset=utf-8");
24 PrintWriter printWriter = response.getWriter();
25 printWriter.write(objectMapper.writeValueAsString("logout success"));
26 printWriter.flush();
27 printWriter.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;
2
3 import io.jsonwebtoken.*;
4
5 import java.util.Date;
6 import java.util.HashMap;
7 import java.util.Map;
8 import java.util.function.Function;
9
10 /**
11 * @Author ChengJianSheng
12 * @Date 2021/5/7
13 */
14 public class JwtUtil {
15
16 private static long TOKEN_EXPIRATION = 24 * 60 * 60 * 1000;
17 private static String TOKEN_SECRET_KEY = "123456";
18
19 /**
20 * 生成Token
21 * @param subject 用户名
22 * @return
23 */
24 public static String createToken(String subject) {
25 long currentTimeMillis = System.currentTimeMillis();
26 Date currentDate = new Date(currentTimeMillis);
27 Date expirationDate = new Date(currentTimeMillis + TOKEN_EXPIRATION);
28
29 // 存放自定义属性,比如用户拥有的权限
30 Map<String, Object> claims = new HashMap<>();
31
32 return 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
41 public static String extractUsername(String token) {
42 return extractClaim(token, Claims::getSubject);
43 }
44
45 public static boolean isTokenExpired(String token) {
46 return extractExpiration(token).before(new Date());
47 }
48
49 public static Date extractExpiration(String token) {
50 return extractClaim(token, Claims::getExpiration);
51 }
52
53 public static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
54 final Claims claims = extractAllClaims(token);
55 return claimsResolver.apply(claims);
56 }
57
58 private static Claims extractAllClaims(String token) {
59 return Jwts.parser().setSigningKey(TOKEN_SECRET_KEY).parseClaimsJws(token).getBody();
60 }
61
62 }

登录成功后,将token返回给客户端

 1 package com.example.demo.handler;
2
3 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 @Component
16 public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
17
18 private static ObjectMapper objectMapper = new ObjectMapper();
19
20 @Override
21 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
22 MyUserDetails myUserDetails = (MyUserDetails) authentication.getPrincipal();
23 String username = myUserDetails.getUsername();
24 String token = JwtUtil.createToken(username);
25 //todo 缓存到 Redis
26 //todo 把token存到Redis中
27
28 response.setContentType("application/json;charset=utf-8");
29 response.getWriter().write(objectMapper.writeValueAsString(token));
30 }
31 }

每次请求过来,从token中取到用户信息,然后放到上下文中

 1 package com.example.demo.filter;
2
3 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 ChengJianSheng
22 * @Date 2021/5/7
23 */
24 public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
25
26 private MyUserDetailsService myUserDetailsService;
27
28 public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
29 super(authenticationManager);
30 }
31
32 public JwtAuthenticationFilter(AuthenticationManager authenticationManager, MyUserDetailsService myUserDetailsService) {
33 super(authenticationManager);
34 this.myUserDetailsService = myUserDetailsService;
35 }
36
37 @Override
38 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
39 String token = request.getHeader("token");
40 System.out.println("请求头中带的token: " + token);
41 if (StringUtils.isNoneBlank(token)) {
42 if (!JwtUtil.isTokenExpired(token)) {
43 String username = JwtUtil.extractUsername(token);
44 if (StringUtils.isNoneBlank(username) && null == SecurityContextHolder.getContext().getAuthentication()) {
45 // 查询用户权限,有以下三种方式:
46 // 1. 可以从数据库中加载
47 // 2. 可以从Redis中加载(PS: 前提是之前已经缓存到Redis中了)
48 // 3. 可以从token中加载(PS: 前提是生成token的时候把用户权限作为Claims放置其中了)
49
50 UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);
51
52 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
53 authRequest.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
54
55 SecurityContextHolder.getContext().setAuthentication(authRequest);
56 }
57 }
58 }
59
60 chain.doFilter(request, response);
61 }
62 }

把这个过滤器添加到

1 http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager(), myUserDetailsService), UsernamePasswordAuthenticationFilter.class);

完整配置如下:

 1 package com.example.demo.config;
2
3 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 @Configuration
22 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
23
24 @Autowired
25 private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
26 @Autowired
27 private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
28 @Autowired
29 private MyAccessDeniedHandler myAccessDeniedHandler;
30 @Autowired
31 private MyLogoutSuccessHandler myLogoutSuccessHandler;
32 @Autowired
33 private MyUserDetailsService myUserDetailsService;
34 @Autowired
35 private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
36 @Autowired
37 private MyAccessDecisionManager myAccessDecisionManager;
38 @Autowired
39 private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
40
41 @Override
42 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
43 auth.userDetailsService(myUserDetailsService)
44 .passwordEncoder(passwordEncoder());
45 }
46
47 @Override
48 protected void configure(HttpSecurity http) throws Exception {
49 http.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
69 http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager(), myUserDetailsService), UsernamePasswordAuthenticationFilter.class);
70
71 http.csrf().disable();
72 }
73
74 @Bean
75 public PasswordEncoder passwordEncoder() {
76 return new BCryptPasswordEncoder();
77 }
78
79 }

增加一个未登录的处理

 1 package com.example.demo.handler;
2
3 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;
7
8 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 ChengJianSheng
16 * @Date 2021/5/7
17 */
18 @Component
19 public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
20
21 private static ObjectMapper objectMapper = new ObjectMapper();
22
23 @Override
24 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
25 response.setContentType("application/json;charset=utf-8");
26 response.getWriter().write(objectMapper.writeValueAsString("未登录,请先登录"));
27 }
28 }

改造后的项目结构如下

最后,用token以后,退出要做一点改动。由于我们采用JWT来生成Token,因此token是没法撤销和删除的,所以此时的退出应该是:

  1. Token生成以后要保存到数据库(MySQL或者Redis)
  2. 每次请求要校验Token是否存在及有效
  3. 退出登录后删除数据库中保存的Token

关于Spring Security实现简单的用户、角色、权限控制就先讲到这里,稍微做一个回顾:

  1. 未认证(登录)的用户提示他要先登录
  2. 已认证的用户判断是否有权限访问

Spring Security 入门篇的更多相关文章

  1. 01 spring security入门篇

    1. 环境搭建 使用SpringBoot搭建开发环境,只需在pom.xml添加如下依赖即可. <?xml version="1.0" encoding="UTF-8 ...

  2. Spring Security入门篇——标签sec:authorize的使用

    Security框架可以精确控制页面的一个按钮.链接,它在页面上权限的控制实际上是通过它提供的标签来做到的 Security共有三类标签authorize authentication accessc ...

  3. Spring boot学习1 构建微服务:Spring boot 入门篇

    Spring boot学习1 构建微服务:Spring boot 入门篇 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框 ...

  4. Java基础-SSM之Spring MVC入门篇

    Java基础-SSM之Spring MVC入门篇 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Spring MVC简介 1>.什么是Spring MVC 答:Sprin ...

  5. Java基础-SSM之Spring快速入门篇

    Java基础-SSM之Spring快速入门篇 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.    Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java ...

  6. SpringBoot集成Spring Security入门体验

    一.前言 Spring Security 和 Apache Shiro 都是安全框架,为Java应用程序提供身份认证和授权. 二者区别 Spring Security:重量级安全框架 Apache S ...

  7. Spring Security 入门(基本使用)

    Spring Security 入门(基本使用) 这几天看了下b站关于 spring security 的学习视频,不得不说 spring security 有点复杂,脑袋有点懵懵的,在此整理下学习内 ...

  8. Spring Security 入门 (二)

    我们在篇(一)中已经谈到了默认的登录页面以及默认的登录账号和密码. 在这一篇中我们将自己定义登录页面及账号密码. 我们先从简单的开始吧:设置自定义的账号和密码(并非从数据库读取),虽然意义不大. 上一 ...

  9. Spring Security 入门(一)

    当你看到这篇文章时,我猜你肯定是碰到令人苦恼的问题了,我希望本文能让你有所收获. 本人几个月前还是 Spring 小白,几个月走来,看了 Spring,Spring boot,到这次的 Spring ...

随机推荐

  1. Codeforces Round #546 C. Nastya Is Transposing Matrices

    题面: 传送门 题目描述: 给出两个n x m的矩阵A,B.矩阵A可以把正方子矩阵进行"转置操作",问:可不可以对矩阵A进行多次这样的操作,使矩阵A变为矩阵B?   题目分析: 这 ...

  2. salesforce零基础学习(一百零二)Limitation篇之 CPU Limit

    本篇参考: https://help.salesforce.com/articleView?id=000339361&type=1&mode=1 https://developer.s ...

  3. Python基础之异常定义

    技术背景 在各类python的项目中,总会涉及到项目自身相关的一些约束条件.这些约束条件体现在,当用户输入的参数或者文件不符合项目要求时,就拒绝这个参数的输入并且播报出来,提醒用户自行修改,而这一过程 ...

  4. 从两个模型带你了解DAOS 分布式异步对象存储

    摘要:分布式异步对象存储 (DAOS) 是一个开源的对象存储系统,专为大规模分布式非易失性内存 (NVM, Non-Volatile Memory) 设计,利用了 SCM(Storage-Class ...

  5. 通过《第一行代码》学习 Android 开发

    第一行代码 Android --第 2 版-- 郭霖 著 第 1 章:开始启程--你的第一行 Android 代码 •1.2 手把手带你搭建开发环境  Android Studio 的安装及配置  A ...

  6. 使用 docker 进行 ElasticSearch + Kibana 集群搭建

    在Docker容器中运行Elasticsearch Kibana和Cerebro 机器信息 10.160.13.139 10.160.9.162 10.160.11.171 1. 安装docker和d ...

  7. [模拟]P1046 陶陶摘苹果

    陶陶摘苹果 ## 题目描述 陶陶家的院子里有一棵苹果树,每到秋天树上就会结出10个苹果.苹果成熟的时候,陶陶就会跑去摘苹果.陶陶有个30厘米高的板凳,当她不能直接用手摘到苹果的时候,就会踩到板凳上再试 ...

  8. noip初赛复习总纲

    初赛复习总纲 目录 初赛复习总纲 计算机发展史 计算机的分类 计算机的应用 操作系统盘点 计算机的基本结构 中央处理器(**CPU**--**Central Processing Unit**) 存储 ...

  9. Img2Latex 临时方法

    Img2Latex 临时方法 博客园Markdown编辑器中公式需采用Latex编写,然鹅现在并不想学习Latex 毕竟工科生,写论文也免不了的各种公式 (终极解决方案当然是学会Latex) 1.工具 ...

  10. 201871030103-陈荟茹 实验二 个人项目―《D{0-1}KP问题》项目报告

    项目 内容 课程班级博客链接 班级博客链接 这个作业要求链接 作业要求链接 我的课程学习目标 1.理解掌握软件设计的过程中的各个环节2.掌握github的使用,将自己的项目上传至githu中 这个作业 ...