扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。
成都创新互联公司于2013年创立,先为玉泉等服务建站,玉泉等地企业,进行企业商务咨询服务。为玉泉企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。
从网上找了一张Spring Security 的工作流程图,如下。
图中标记的MyXXX,就是我们项目中需要配置的。
表结构
建表语句
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS `role`;
DROP TABLE IF EXISTS `user_role`;
DROP TABLE IF EXISTS `role_permission`;
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `user_role` (
`user_id` bigint(11) NOT NULL,
`role_id` bigint(11) NOT NULL
);
CREATE TABLE `role_permission` (
`role_id` bigint(11) NOT NULL,
`permission_id` bigint(11) NOT NULL
);
CREATE TABLE `permission` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`description` varchar(255) NULL,
`pid` bigint(11) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO user (id, username, password) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e');
INSERT INTO user (id, username , password) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e');
INSERT INTO role (id, name) VALUES (1,'USER');
INSERT INTO role (id, name) VALUES (2,'ADMIN');
INSERT INTO permission (id, url, name, pid) VALUES (1,'/user/common','common',0);
INSERT INTO permission (id, url, name, pid) VALUES (2,'/user/admin','admin',0);
INSERT INTO user_role (user_id, role_id) VALUES (1, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 2);
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.thymeleaf.extras
thymeleaf-extras-security4
spring:
thymeleaf:
mode: HTML5
encoding: UTF-8
cache: false
datasource:
driver-class-name: com.MySQL.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring-security?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
public class User implements UserDetails , Serializable {
private Long id;
private String username;
private String password;
private List authorities;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public List getAuthorities() {
return authorities;
}
public void setAuthorities(List authorities) {
this.authorities = authorities;
}
/**
* 用户账号是否过期
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 用户账号是否被锁定
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 用户密码是否过期
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 用户是否可用
*/
@Override
public boolean isEnabled() {
return true;
}
}
上面的 User 类实现了 UserDetails 接口,该接口是实现Spring Security 认证信息的核心接口。其中 getUsername 方法为 UserDetails 接口 的方法,这个方法返回 username,也可以是其他的用户信息,例如手机号、邮箱等。getAuthorities() 方法返回的是该用户设置的权限信息,在本实例中,从数据库取出用户的所有角色信息,权限信息也可以是用户的其他信息,不一定是角色信息。另外需要读取密码,最后几个方法一般情况下都返回 true,也可以根据自己的需求进行业务判断。
public class Role implements GrantedAuthority {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getAuthority() {
return name;
}
}
Role 类实现了 GrantedAuthority 接口,并重写 getAuthority() 方法。权限点可以为任何字符串,不一定非要用角色名。
所有的Authentication实现类都保存了一个GrantedAuthority列表,其表示用户所具有的权限。GrantedAuthority是通过AuthenticationManager设置到Authentication对象中的,然后AccessDecisionManager将从Authentication中获取用户所具有的GrantedAuthority来鉴定用户是否具有访问对应资源的权限。
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//查数据库
User user = userMapper.loadUserByUsername( userName );
if (null != user) {
List roles = roleMapper.getRolesByUserId( user.getId() );
user.setAuthorities( roles );
}
return user;
}
}
Service 层需要实现 UserDetailsService 接口,该接口是根据用户名获取该用户的所有信息, 包括用户信息和权限点。
@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionMapper permissionMapper;
/**
* 每一个资源所需要的角色 Collection决策器会用到
*/
private static HashMap> map =null;
/**
* 返回请求的资源需要的角色
*/
@Override
public Collection getAttributes(Object o) throws IllegalArgumentException {
if (null == map) {
loadResourceDefine();
}
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
for (Iterator it = map.keySet().iterator() ; it.hasNext();) {
String url = it.next();
if (new AntPathRequestMatcher( url ).matches( request )) {
return map.get( url );
}
}
return null;
}
@Override
public Collection getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
/**
* 初始化 所有资源 对应的角色
*/
public void loadResourceDefine() {
map = new HashMap<>(16);
//权限资源 和 角色对应的表 也就是 角色权限 中间表
List rolePermissons = permissionMapper.getRolePermissions();
//某个资源 可以被哪些角色访问
for (RolePermisson rolePermisson : rolePermissons) {
String url = rolePermisson.getUrl();
String roleName = rolePermisson.getRoleName();
ConfigAttribute role = new SecurityConfig(roleName);
if(map.containsKey(url)){
map.get(url).add(role);
}else{
List list = new ArrayList<>();
list.add( role );
map.put( url , list );
}
}
}
}
MyInvocationSecurityMetadataSourceService 类实现了 FilterInvocationSecurityMetadataSource,FilterInvocationSecurityMetadataSource 的作用是用来储存请求与权限的对应关系。
FilterInvocationSecurityMetadataSource接口有3个方法:
/**
* 决策器
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class);
/**
* 通过传递的参数来决定用户是否有访问对应受保护对象的权限
*
* @param authentication 包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中设置的authorities。
* @param object 就是FilterInvocation对象,可以得到request等web资源
* @param configAttributes configAttributes是本次访问需要的权限
*/
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (null == configAttributes || 0 >= configAttributes.size()) {
return;
} else {
String needRole;
for(Iterator iter = configAttributes.iterator(); iter.hasNext(); ) {
needRole = iter.next().getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority().trim())) {
return;
}
}
}
throw new AccessDeniedException("当前访问没有权限");
}
}
/**
* 表示此AccessDecisionManager是否能够处理传递的ConfigAttribute呈现的授权请求
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
/**
* 表示当前AccessDecisionManager实现是否能够为指定的安全对象(方法调用或Web请求)提供访问控制决策
*/
@Override
public boolean supports(Class> aClass) {
return true;
}
}
MyAccessDecisionManager 类实现了AccessDecisionManager接口,AccessDecisionManager是由AbstractSecurityInterceptor调用的,它负责鉴定用户是否有访问对应资源(方法或URL)的权限。
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public Class> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
每种受支持的安全对象类型(方法调用或Web请求)都有自己的拦截器类,它是AbstractSecurityInterceptor的子类,AbstractSecurityInterceptor 是一个实现了对受保护对象的访问进行拦截的抽象类。
AbstractSecurityInterceptor的机制可以分为几个步骤:
AbstractSecurityInterceptor中的方法说明:
了解了AbstractSecurityInterceptor,就应该明白了,我们自定义MyFilterSecurityInterceptor就是想使用我们之前自定义的 AccessDecisionManager 和 securityMetadataSource。
@EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security。自定义类 继承了WebSecurityConfigurerAdapter来重写了一些方法来指定一些特定的Web安全设置。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService userService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//校验用户
auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() {
//对密码进行加密
@Override
public String encode(CharSequence charSequence) {
System.out.println(charSequence.toString());
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
//对密码进行判断匹配
@Override
public boolean matches(CharSequence charSequence, String s) {
String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
boolean res = s.equals( encode );
return res;
}
} );
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","index","/login","/login-error","/401","/css/**","/js/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage( "/login" ).failureUrl( "/login-error" )
.and()
.exceptionHandling().accessDeniedPage( "/401" );
http.logout().logoutSuccessUrl( "/" );
}
}
@Controller
public class MainController {
@RequestMapping("/")
public String root() {
return "redirect:/index";
}
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/login-error")
public String loginError(Model model) {
model.addAttribute( "loginError" , true);
return "login";
}
@GetMapping("/401")
public String accessDenied() {
return "401";
}
@GetMapping("/user/common")
public String common() {
return "user/common";
}
@GetMapping("/user/admin")
public String admin() {
return "user/admin";
}
}
login.html
登录
Login page
用户名或密码错误
index.html
首页
page list
common page
admin page
admin.html
admin page
success admin page!!!
common.html
common page
success common page!!!
401.html
401 page
权限不够
拒绝访问!
最后运行项目,可以分别用 user、admin 账号 去测试认证和授权是否正确。
《深入理解Spring Cloud与微服务构建》
https://www.ktanx.com/blog/p/4929
https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-security
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流