本篇介绍如何在SpringBoot下整合SpringSecurity,使用用户角色模块权限模型,自定义处理登录、密码校验,取权限控制,实现用户-模块-角色的权限模型,并基于URL拦截(自行控制AccessDecisionManager,匹配用户所属的角色和请求资源所属的角色,FilterInvocationSecurityMetadataSource,实现请求资源对应的角色)
因为很多地方环节,能实现自定义了,就能整理把握,实现随意的控制和增加自己的业务逻辑。
用户/角色/模块权限模型介绍
这里使用的是RBAC(Role-Based Access Control,基于角色的访问控制),用户通过角色与模块/权限进行关联。某个用户拥有多个角色,每个角色拥有多个模块的权限。这样就能查询出某个用户,所拥有的模块的权限,根据这个来决定该用户是否拥有某个模块的操作权限,如下图所示:
数据库建表
- 用户表
CREATE TABLE `t_account` ( `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID', `create_date` datetime DEFAULT NULL COMMENT '新建日期', `update_date` datetime DEFAULT NULL COMMENT '更新日期', `no` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '登录帐号', `password` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码', `name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '姓名', `mobile` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号', `disabled` int NOT NULL DEFAULT '0' COMMENT '是否禁用 0启用 1禁用', PRIMARY KEY (`id`), UNIQUE KEY `t_account_uk1` (`no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';
- 角色表
CREATE TABLE `t_role` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称', `disabled` int NOT NULL DEFAULT '0' COMMENT '是否禁用 1禁用', `remark` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='角色表';
- 模块表(权限表)
CREATE TABLE `t_module` ( `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名称', `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'URL', `disabled` int NOT NULL DEFAULT '0' COMMENT '是否禁用 1禁用', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='模块(菜单)表';
- 用户角色授权表
CREATE TABLE `t_role_assign` ( `id` int NOT NULL AUTO_INCREMENT, `account_id` int DEFAULT NULL, `role_id` int DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户和角色对应表';
- 角色模块授权表
CREATE TABLE `t_module_role` ( `module_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `role_id` int NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='模块(菜单)和角色对应关系表';
pom增加相关依赖
增加spring-boot-starter-security,如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.terrynow</groupId> <artifactId>test</artifactId> <version>0.0.1-SNAPSHOT</version> <name>test</name> <description>test</description> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.4.3.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>6.0.13.Final</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
准备POJO Entity类
Entity类仅供参考,我使用了Hibernate的多对多,其他ORM请修改
Account.java 需要extends SpringSecurity的UserDetails,如下:
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import javax.persistence.*; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; /** * @date 2019-06-18 14:31 * @description */ @Entity @Table(name = "t_account") public class Account implements Serializable, UserDetails { private Long id; private String no; private String name; private String mobile; private String password; private Date createDate; private Date updateDate; private boolean disabled;//是否禁用 private List<Role> roles = new ArrayList<Role>(); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Transient @Override public Collection<? extends GrantedAuthority> getAuthorities() { Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>(); if (isDisabled()) { return authSet; } // 获取用户对应 角色集合 List<Role> roles = getRoles(); //组装成spring需要的格式,其它的就交给spring进行验证 for (Role role : roles) { // System.out.println("取得用户所属的角色:" + role.getName()); if (!role.isDisabled()) { authSet.add(new SimpleGrantedAuthority(String.valueOf(role.getId()))); } //authSet.add(new GrantedAuthorityImpl(role.getId())); } return authSet; } public String getPassword() { return password; } @Transient @Override public String getUsername() { return no; } @Transient @Override public boolean isAccountNonExpired() { return true; } @Transient @Override public boolean isAccountNonLocked() { return !isDisabled(); } @Transient @Override public boolean isCredentialsNonExpired() { return true; } @Transient @Override public boolean isEnabled() { return !isDisabled(); } public String getNo() { return no; } public void setNo(String no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Column(name = "create_date") public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } @Column(name = "update_date") public Date getUpdateDate() { return updateDate; } public void setUpdateDate(Date updateDate) { this.updateDate = updateDate; } public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } public void setPassword(String password) { this.password = password; } public String getMobile() { return mobile; } public void setMobile(String mobile) { this.mobile = mobile; } @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinTable(name = "t_role_assign", joinColumns = @JoinColumn(name = "account_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } //以下两行要加,不然spring security不能控制session public boolean equals(Object rhs) { return rhs instanceof Account && this.no.equals(((Account) rhs).no); } public int hashCode() { return this.no.hashCode(); } }
Role.java
import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.ManyToMany; import javax.persistence.Table; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * @date 2016-1-7 10:29 * @description */ @Entity @Table(name = "t_role") public class Role implements Serializable { private Long id; private String name; private boolean disabled;//是否禁用 private String remark;//备注 private List<Account> accounts = new ArrayList<>(); private List<Module> modules = new ArrayList<Module>(); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) 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; } @ManyToMany(mappedBy = "roles", cascade = CascadeType.ALL, fetch = FetchType.LAZY) public List<Account> getAccounts() { return accounts; } public void setAccounts(List<Account> accounts) { this.accounts = accounts; } @ManyToMany(mappedBy = "mroles", cascade = CascadeType.ALL, fetch = FetchType.EAGER) public List<Module> getModules() { return modules; } public void setModules(List<Module> modules) { this.modules = modules; } public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } }
Module.java
import javax.persistence.*; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * @date 2016-1-7 10:32 * @description */ @Entity @Table(name = "t_module") public class Module implements Serializable { private String id; private String name; private String url; private boolean disabled;//是否禁用 private List<Role> mroles = new ArrayList<Role>(); @Id @Column(length = 36) @GeneratedValue(generator = "uuid2") @GenericGenerator(name = "uuid2", strategy = "org.hibernate.id.UUIDGenerator") public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinTable(name = "t_module_role", joinColumns = @JoinColumn(name = "module_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) public List<Role> getMroles() { return mroles; } public void setMroles(List<Role> mroles) { this.mroles = mroles; } @Column(name = "url", unique = true) public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } }
增加SpringSecurity的配置类
新增一个package,com.terrynow.test.security,新增类:WebSecurityConfig.java
主要是配置了mySecurityMetadataSource(角色提供管理器)、myAccessDecisionManager(决策管理器)、登录校验(myAuthenticationProvider)
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.web.filter.CharacterEncodingFilter; import javax.sql.DataSource; /** * @author Terry E-mail: yaoxinghuo at 126 dot com * @date 2021-7-7 16:21 * @description */ @Configuration //@EnableWebSecurity(debug = true) @EnableWebSecurity() @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { public static final String SECURITY_KEY = "test"; @Autowired private MyUserDetailService myUserDetailService; @Autowired private MyLogoutHandler myLogoutHandler; @Autowired private MyAuthenticationProvider myAuthenticationProvider; @Autowired private DataSource dataSource; @Autowired private MyAccessDecisionManager myAccessDecisionManager; @Autowired private MySecurityMetadataSource mySecurityMetadataSource; @Bean public AuthenticationSuccessHandler myAuthenticationSuccessHandler() { return new MyAuthenticationSuccessHandler(); } @Bean public SessionRegistry sessionRegistry() { SessionRegistry sessionRegistry = new SessionRegistryImpl(); return sessionRegistry; } @Bean @Override public UserDetailsService userDetailsService() { return myUserDetailService; } @Override public void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(myAuthenticationProvider); } @Override protected void configure(HttpSecurity http) throws Exception { //防止用户名输入了中文,后面获取到了乱码 CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); filter.setForceEncoding(true); http.addFilterBefore(filter, CsrfFilter.class); //自定义的登录form提交链接,j_spring_security_check是为了和之前SpringWeb的一致,在login的html页面也要一样 String loginProcessingUrl = "/j_spring_security_check"; http.headers().frameOptions().disable();//可以嵌套到frame中去 //login?code=-2发现不能加斜线,要不然如果http://xxx/new 前面带东西,会跳转不对 myLogoutHandler.setDefaultTargetUrl("/login?code=1"); http .authorizeRequests() .antMatchers(loginProcessingUrl, "/main/**", "/login*").permitAll() //任何人都可以访问 .anyRequest().authenticated() //其他所有资源都需要认证,登陆后访问 // 这里使用withObjectPostProcessor来实现自定义的决策管理和资源角色管理器 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O fsi) { fsi.setAccessDecisionManager(myAccessDecisionManager); fsi.setSecurityMetadataSource(mySecurityMetadataSource); return fsi; } }) .and() .formLogin().loginPage("/login").loginProcessingUrl(loginProcessingUrl) .successHandler(myAuthenticationSuccessHandler()) // 登陆成功处理器 .failureHandler(new MyAuthFailHandler("/login?code=-1")) // 登陆失败处理器 .authenticationDetailsSource(new MyWebAuthenticationDetailsSource()) .permitAll() .and().exceptionHandling().accessDeniedPage("/login?code=-3") .and().logout().logoutUrl("/logout").logoutSuccessHandler(myLogoutHandler).permitAll() .and().csrf().disable() ; //UserDetails实现类Account那边要 重写equals()和hashCode() http.sessionManagement().maximumSessions(5).expiredUrl("/login?code=-5").sessionRegistry(sessionRegistry()); } @Override public void configure(WebSecurity web) { //解决静态资源被拦截的问题 web.ignoring().antMatchers("/assets/**", "/assets_mnt/**", "/api/**", "/dashboard/**"); web.ignoring().antMatchers("/**/fav.ico", "/favicon.ico", "/robots.txt"); web.ignoring().antMatchers("/**/captcha.jpg"); web.ignoring().antMatchers("/ping", "/test", "/404_error", "/generic_error", "/error"); } }
其他SpringSecurity用到的自定义类
直接上代码,大部分都会在代码里做相关的注释,如有问题,请稍加修改再取用。
LoginController.java,从配置文件里可以看到我们把登录的url的定义为/login?code=xxx,根据xxx来判断一些登录的状态,如下:
@RequestMapping(value = "/login", method = RequestMethod.GET) public ModelAndView login(HttpSession session, @RequestParam(value = "code", required = false) String codeStr) { ModelAndView model = new ModelAndView(); String message = null; int code = 0; if (codeStr != null) { try { code = Integer.parseInt(codeStr); } catch (NumberFormatException ignored) { } } switch (code) { case -1: message = "登录失败"; break; case -2: message = "验证码错误"; break; case -3: message = "您无权访问指定资源,请登录到具有适当权限的用户"; break; case -4: message = "管理员已重新设定权限,请重新登录"; break; case -5: message = "请重新登录";//可能是session太多,超过了 break; case -7: message = "账户已停用"; break; case 1: message = "已成功注销"; break; } if (message != null) model.addObject("message", message); model.setViewName("login"); return model; }
我做了一个简单的vue的login.jsp页面,如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%-- ~ Copyright (c) 2022. ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. ~ --%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>登录</title> <!-- Tell the browser to be responsive to screen width --> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" href="${base}/assets/css/iview.css"> <script type="text/javascript" src="assets/js/vue.min.js"></script> <script type="text/javascript" src="assets/js/iview.min.js"></script> <script type="text/javascript" src="assets/js/axios.min.js"></script> <script> window.Promise || document.write('<script src="assets/js/es6-promise.auto.min.js"><\/script><script type="text/javascript" src="assets/js/es6-shim.min.js"><\/script>'); </script> <style type="text/css"> #app, body, html { -webkit-font-smoothing: antialiased; width: 100%; height: 100%; position: relative; } .img-responsive { width: 100%; height: auto; } .base-app-style { /*min-width: 768px;*/ position: relative; height: 100%; width: 100%; /*background: #3075ab;*/ /*background: linear-gradient(to bottom,#343A40 0,#4B545C 100%);*/ /*filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, StartColorStr='$fromColor', EndColorStr='$toColor');*/ <%--background: url('${base}/assets/image/login_bg.jpg') no-repeat center center fixed;--%> -webkit-background-size: cover; -moz-background-size: cover; -o-background-size: cover; background-size: cover; /*background-size: 100%;*/ } .footer { /*background-color: #000;*/ position: fixed; left: 0; right: 0; bottom: 0; font-weight: 700; line-height: 1.4; padding: 10px; color: #7B7E81; text-align: center; } .gradient-text-one{ /*background-image:-webkit-linear-gradient(bottom,#0B649E,#107596,#168C8C);*/ /*-webkit-background-clip:text;*/ /*-webkit-text-fill-color:transparent;*/ /*text-shadow: 0px 1px 1px gray;*/ color: #0B649E; } @media screen and (max-width: 769px){ #app { padding: 0 15px; } .mobile-none { display: none; } #product { font-size: 34px; font-weight: 600; text-align: center; } } @media screen and (min-width: 769px){ #product { top: calc(50% - 300px); /*left:50%;*/ /*transform:translate(-50%,0);*/ position: absolute; font-size: 44px; font-weight: 600; width: 100%; text-align: center; } #downloads { margin-top: 10px; } #downloads .downloads{ display: flex; text-align: center; margin-top: 4px; } #downloads a { flex: 1; } #downloads a img { width:48px; height:48px; margin-bottom: 4px; } #login { width: 320px; left:50%; transform:translate(-50%,0); position: absolute; top: calc(50% - 150px); } .base-app-style { min-width: 768px; } .pc-none { display: none; } } </style> </head> <body class="base-app-style login-page"> <div id="app"> <div style="padding: 40px;" id="product"> <div class="gradient-text-one" style=""><%=Constants.SYSTEM_NAME%></div> </div> <div id="login"> <card> <c:if test="${empty message}"> <p slot="title">用户登录</p> </c:if> <i-form id="loginform" ref="loginform" action="${base}/j_spring_security_check" method="post" @submit.native.prevent="handleSubmit()"> <c:if test="${not empty message}"> <alert type="error">${message}</alert> </c:if> <div> <%-- <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>--%> <div> <i-input placeholder="请输入用户名" size="large" style="margin-bottom:10px;" v-model="username" name="username" value='<c:out value="${sessionScope.LAST_USERNAME}"/>'> <icon type="md-person" size="20" slot="prepend"/> </i-input> </div> <div> <i-input placeholder="请输入密码" type="password" size="large" style="margin-bottom:10px;" v-model="password" name="password" value='<c:out value="${sessionScope.LAST_PASSWORD}"/>'> <icon type="md-lock" size="20" slot="prepend"/> </i-input> </div> <row style="margin-bottom:10px;"> <i-col span="12"> <i-input name="captcha" v-model="captcha" size="large" placeholder="请输入验证码"> <icon type="md-checkmark" size="20" slot="prepend" /> </i-input> </i-col> <i-col span="12"><img :src="captchasrc" @click="refreshCaptcha" class="img-responsive" style="margin-left: 5px;"/></i-col> </row> <row> <i-col span="12"> <Checkbox name='remember-me' size="large">保持登录</Checkbox> </i-col> <i-col span="12"> <%-- <tooltip placement="top-start">--%> <%-- <div slot="content" style="text-align:left;">--%> <%-- <p>请联系XX</p>--%> <%-- </div>--%> <%-- <span style="color:#565D70;font-size: 16px;">忘记密码 <icon type="ios-help-circle" size="20" prepend/></span>--%> <%-- </tooltip>--%> </i-col> </row> <i-button html-type="submit" :loading="loading" type="primary" long style="margin-top: 20px;" size="large"> <span v-if="loading">登录中</span> <span v-else>登录</span> </i-button> </div> </i-form> </card> </div> <!-- /.login-box --> <div id="footer" class="footer mobile-none"> Footer </div> </div> <script> new Vue({ el: '#app', data: { username:'<c:out value="${sessionScope.LAST_USERNAME}"/>', password:'<c:out value="${sessionScope.LAST_PASSWORD}"/>', loading: false }, created: function () { this.refreshCaptcha(); }, methods: { handleSubmit: function() { this.loading = true; document.getElementById('loginform').submit(); }, refreshCaptcha: function() { this.captchasrc='captcha.jpg?r='+Math.random(); this.captcha = ''; }, } }) </script> </body> </html>
MySecurityMetadataSource.java
import cn.org.pcoic.manage.entity.Account; import cn.org.pcoic.manage.entity.Module; import cn.org.pcoic.manage.entity.Role; import cn.org.pcoic.manage.service.intf.ISystemService; import cn.org.pcoic.manage.util.Constants; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author Terry E-mail: yaoxinghuo at 126 dot com * @date Jan 22, 2015 3:00:04 PM * @description 返回请求资源对应的角色 */ @Service public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private static final Log log = LogFactory.getLog(MySecurityMetadataSource.class); @Resource(name = "systemService") private ISystemService systemService; public static long ACCOUNT_LOAD_TIME_MIN = 0;//account里的loadTime如果小于这个时间,就要重新加载auth private static Map<String, Collection<ConfigAttribute>> resourceMap = null; //这里存的是完整菜单,如果要输出菜单,还要根据当前登录的用户 private static Map<Integer, List<Module>> menuModules = new HashMap<>(); public static List<Module> menuModules2 = new ArrayList<>(); public static String[] anyoneAccessUris = new String[]{"/login", "/logout", "/public"}; @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return new ArrayList<>(); } public static void reloadResourceDefineNextTime() { log.warn("set resourceMap null, will reload resource definition next time."); resourceMap = null; } public static Module getMenuModuleForUrl(String url) { if (url.equals("/")) {//FIXME 首页,暂时这么写死 Module module = new Module(); module.setName(Constants.HOME_MODULE_NAME); module.setUrl("/"); module.setMenuLev(0); return module; } return menuModules2.stream().filter(module -> url.equals(module.getUrl())).findFirst().orElse(null); // for (Module module : menuModules2) { // if (url.equals(module.getUrl())) // return module; // } // return null; } public static List<Module> getMenuModule(int menuIndex, Account account) { if (account == null) { return getMenuModuleForCurrentUser(menuIndex); } return getMenuModule(menuIndex, account.getAuthorities()); } public static List<Module> getMenuModule(int menuIndex) { return menuModules.get(menuIndex); } public static List<Module> getMenuModuleForCurrentUser(int menuIndex) { List<Module> modules = new ArrayList<>(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) return modules; return getMenuModule(menuIndex, authentication.getAuthorities()); } public static List<Module> getMenuModule(int menuIndex, Collection<? extends GrantedAuthority> authorities) { List<Module> modules = new ArrayList<>(); List<Module> modules1 = menuModules.get(menuIndex); if (modules1 != null) { for (Module module : modules1) { if (isUserCanAccessModule(authorities, module)) { if (module.isShowInMenu()) modules.add(module); } } } return modules; } public static boolean isUserCanAccessModule(Collection<? extends GrantedAuthority> authorities, Module module) { List<Role> roles = module.getMroles(); for (Role role : roles) { if (!role.isDisabled() && isUserHasRole(authorities, role)) return true; } return false; } public static String isCurrentUserCanAccessUrlHtml(String url, String base, String name, String prefix) { if (!isCurrentUserCanAccessUrl(url)) { return ""; } Module module = getMenuModuleForUrl(url); if (module == null) { return ""; } return prefix + "<a href='" + base + url + "'>﹥" + (StringUtils.isEmpty(name) ? module.getName() : name) + "</a>"; } public static boolean isCurrentUserCanAccessUrl(String url) { if (StringUtils.isEmpty(url)) { return false; } Account user = null; Object p = SecurityContextHolder.getContext().getAuthentication() == null ? null : SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (p == null || p instanceof String) { } else { user = (Account) p; } if (user == null) { return false; } // if (url.equals("/")) {//首页不加权限,直接能访问 // return true; // } Module module = getMenuModuleForUrl(url); if (module == null) { return true; } return isUserCanAccessModule(user.getAuthorities(), module); } private static boolean isUserHasRole(Collection<? extends GrantedAuthority> authorities, Role role) { for (GrantedAuthority authority : authorities) { if (String.valueOf(role.getId()).equals(authority.getAuthority())) { return true; } } return false; } public static Collection<ConfigAttribute> getConfigAttributeForModuleUrl(String url) { if (resourceMap == null) return null; return resourceMap.get(url); } //加载所有资源与权限的关系 private synchronized void loadResourceDefine() { //如果Role或者Module改变了,只要SecurityMetadataSource.reloadResourceDefineNextTime(); 那下次就用重新加载了 if (resourceMap != null) return; resourceMap = new HashMap<>(); menuModules.clear(); menuModules2.clear(); List<Module> resources = systemService.listModules(); for (Module module : resources) { if (module.isDisabled()) { continue; } if (module.getMenuLev() == 0) { Constants.HOME_MODULE_NAME = module.getName(); } menuModules2.add(module); int menuIndex = (module.getMenuLev() - module.getMenuLev() % 100) / 100; List<Module> modules = menuModules.get(menuIndex); if (modules == null) { modules = new ArrayList<>(); } modules.add(module); menuModules.put(menuIndex, modules); //获得拥有此权限的所有角色 List<Role> roles = module.getMroles(); Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>(); //以角色ID名封装为Spring的security Object,此后交给spring进行管理 for (Role role : roles) { if (role.isDisabled()) { continue; } // System.out.println("requestUrl对应的角色 " + role.getName()); ConfigAttribute configAttribute = new SecurityConfig(String.valueOf(role.getId())); configAttributes.add(configAttribute); } //如果还没有指定可以访问的角色,那么硬编码成普通用户可以看 if (configAttributes.size() == 0) { configAttributes.add(new SecurityConfig("1")); } resourceMap.put(module.getUrl(), configAttributes); } // Set<Map.Entry<String, Collection<ConfigAttribute>>> resourceSet = // resourceMap.entrySet(); // System.out.println(resourceSet.isEmpty()); // for // (Map.Entry<String, Collection<ConfigAttribute>> entry : resourceSet) { // System.out.println(entry.getKey() + "," + entry.getValue()); // } // Iterator<Map.Entry<String, Collection<ConfigAttribute>>> iterator = // resourceSet.iterator(); } /** * 返回所请求资源对应的角色 */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { //截取问号前面的URL进行判断,http://localhost:8080/sys/userlist?page=0,这样的URL数据库中没有, //故访问不到,数据库中只存储了不带问号的 String originalUrl = ((FilterInvocation) object).getRequestUrl(); String requestUrl = parserUrl(originalUrl); // log.info("originalUrl:" + originalUrl + ", parsed: " + requestUrl); //这里强制让缓存清空,调试用的!!! // resourceMap = null; if (resourceMap == null) { loadResourceDefine(); } boolean isCanAccess = false; for (String accessUris : anyoneAccessUris) { if (accessUris.equals(requestUrl)) { isCanAccess = true; break; } } if (requestUrl.startsWith("/public/")) { isCanAccess = true; } if (isCanAccess) { return null; } Collection<ConfigAttribute> configAttributes = resourceMap.get(requestUrl); if (configAttributes == null || configAttributes.size() == 0) { if (requestUrl.contains("/WEB-INF/")) { return null; } // if (requestUrl.equals("/")) {//首页不要加权限了,直接能访问 // return null; // } log.warn("no module for request url: " + requestUrl); configAttributes = new ArrayList<>(); configAttributes.add(new SecurityConfig("0"));//加一个不存在的role,让这个无法访问,必须要手动到模块那边去添加 // if (!Constants.PRODUCT_ENV) {//开发环境就创建这个URL // systemService.createModuleForUrl(requestUrl); // } } return configAttributes; } /** * Url可能是/sys/employeesMap/list 我们只要前面2个就可以了 * * @param requestUrl * @return */ private String parserUrl(String requestUrl) { if (StringUtils.isEmpty(requestUrl) || requestUrl.equals("/")) return "/"; String[] parts = requestUrl.split("/"); if (parts.length < 4) { return removeParameterFromUrl(requestUrl); } else { return removeParameterFromUrl(parts[0] + "/" + parts[1] + "/" + parts[2]); } } /** * 去掉url后面的?#|什么的参数 * * @param requestUrl * @return */ private String removeParameterFromUrl(String requestUrl) { Pattern p = Pattern.compile("([\\.//_-]*\\w)*"); Matcher m = p.matcher(requestUrl); if (m.find()) { return m.group(); } return requestUrl; } @Override public boolean supports(Class<?> arg0) { return true; } }
MyAccessDecisionManager.java
import cn.org.pcoic.manage.entity.Account; import cn.org.pcoic.manage.service.intf.ISystemService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Collection; /** * @author Terry E-mail: yaoxinghuo at 126 dot com * @date Jan 22, 2015 2:55:08 PM * @description 匹配用户所属的角色和请求资源所属的角色,如果能匹配的上,则访问请求的资源, * 否则没有权限访问,会跳转到login.jsp页面 * spring给你做决定 * spring把一些东西交给你判断 * 你觉得根据这些东西决定是否允许用户操作 */ @Component public class MyAccessDecisionManager implements AccessDecisionManager { private static final Log log = LogFactory.getLog(MyAccessDecisionManager.class); @Resource(name = "systemService") private ISystemService systemService; public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if (configAttributes == null) { //现在数据库中没有的URL,这里都会直接显示页面,目前还没处理 throw new AccessDeniedException("访问路径不存在!"); } if (authentication.getPrincipal() != null && authentication.getPrincipal() instanceof Account && ((Account) authentication.getPrincipal()).loadTime < MySecurityMetadataSource.ACCOUNT_LOAD_TIME_MIN) { Account account = systemService.getAccountByNo(authentication.getName()); Authentication newAuth = new UsernamePasswordAuthenticationToken(account, account.getPassword(), account.getAuthorities()); authentication = newAuth; log.warn("reload new auth for: " + account.getNo() + ", time: " + account.loadTime); SecurityContextHolder.getContext().setAuthentication(newAuth); } // 所请求的资源对应的角色(一个资源对多个角色) for (ConfigAttribute configAttribute : configAttributes) { // 是role-id String neededRoleId = configAttribute.getAttribute(); if("0".equals(neededRoleId) && !isAnonymous(authentication)) {//MySecurityMetadataSource的277行,首页模块不存在,就加了个roleID是0,登录用户能访问 return; } // 用户所拥有的权限authentication for (GrantedAuthority ga : authentication.getAuthorities()) { // System.out.println("用户所拥有的权: " + ga.getAuthority()); if (neededRoleId.equals(ga.getAuthority())) { //若请求的权限在用户拥有的权限内,则请求成功 return; } } } // 没有权限会跳转到login.jsp页面 throw new AccessDeniedException("没有权限访问"); } //判断是否是匿名访问 private boolean isAnonymous(Authentication authentication) { if (authentication.getAuthorities() == null || authentication.getAuthorities().size() == 0) { return true; } if (authentication.getAuthorities().size() == 1) { for (GrantedAuthority ga : authentication.getAuthorities()) { if (ga.getAuthority().equals("ROLE_ANONYMOUS")) { return true; } } } return false; } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
MyAuthenticationProvider.java
import cn.org.pcoic.manage.entity.Account; import cn.org.pcoic.manage.service.intf.ISystemService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; import javax.annotation.Resource; /** * @author Terry E-mail: yaoxinghuo at 126 dot com * @date Jul 8, 2015 2:28:55 PM * @description */ @Component public class MyAuthenticationProvider implements AuthenticationProvider { private static final Log log = LogFactory.getLog(MyAuthenticationProvider.class); @Resource(name = "systemService") private ISystemService systemService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String no = authentication.getName(); String password = authentication.getCredentials().toString(); Object details = authentication.getDetails(); String ipAddress = null; if (details instanceof MyWebAuthenticationDetails) ipAddress = ((MyWebAuthenticationDetails) details).getRemoteAddress(); BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); String passwordNotMatch = "用户名或密码错误"; Account account = systemService.getAccountByNo(no); // 数据库中查询此用户 if (account == null) { // throw new UsernameNotFoundException(Constants.AUTH_WEB); throw new BadCredentialsException(passwordNotMatch); } if (!account.isEnabled() || !account.isAccountNonLocked()) { throw new LockedException("账户已停用"); } boolean matches = passwordEncoder.matches(password, account.getPassword()); if (!matches) { throw new BadCredentialsException(passwordNotMatch); } log.warn(WebSecurityConfig.SECURITY_KEY + " 密码登录,姓名:" + account.getName() + ",帐号:" + account.getNo() + ",ID:" + account.getId() + ",IP:" + ipAddress); systemService.updateAccountLastLogin(account.getId(), ipAddress); return new UsernamePasswordAuthenticationToken(account, password, account.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
MyAuthenticationSuccessHandler.java
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.core.Authentication; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.WebAttributes; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; /** * @author Terry E-mail: yaoxinghuo at 126 dot com * @date 2019-07-08 19:49 * @description */ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private static final Log log = LogFactory.getLog(MyAuthenticationSuccessHandler.class); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { handle(request, response, authentication); clearAuthenticationAttributes(request); } protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String targetUrl = determineTargetUrl(authentication); if (response.isCommitted()) { log.debug("Response has already been committed. Unable to redirect to " + targetUrl); return; } redirectStrategy.sendRedirect(request, response, targetUrl); } protected String determineTargetUrl(Authentication authentication) { return "/"; // boolean isAdmin = false; // Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // for (GrantedAuthority grantedAuthority : authorities) { // if (grantedAuthority.getAuthority().equals("1")) { // isAdmin = true; // break; // } // } // // if (isAdmin) { // return "/sys/accounts"; // } else { // return "/"; //// throw new IllegalStateException(); // } } protected void clearAuthenticationAttributes(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session == null) { return; } session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); } public void setRedirectStrategy(RedirectStrategy redirectStrategy) { this.redirectStrategy = redirectStrategy; } protected RedirectStrategy getRedirectStrategy() { return redirectStrategy; } }
MyAuthFailHandler.java
import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; /** * @author Terry E-mail: yaoxinghuo at 126 dot com * @date 2015-9-14 09:57 * @description 如果登录发生错误,记住用户名 参考: * http://stackoverflow.com/questions/15052860/spring-security-authentication-get-username-without-spring-security-last-username */ public class MyAuthFailHandler extends SimpleUrlAuthenticationFailureHandler { public static final String LAST_USERNAME_KEY = "LAST_USERNAME"; public static final String LAST_PASSWORD_KEY = "LAST_PASSWORD"; public static final String LAST_AUTH_FAIL_MSG = "LAST_AUTH_FAIL_MSG"; public MyAuthFailHandler(String defaultFailureUrl){ super(defaultFailureUrl); } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { super.onAuthenticationFailure(request, response, exception); String lastUserName = request.getParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY); HttpSession session = request.getSession(false); if (session != null || isAllowSessionCreation()) { session.setAttribute(LAST_USERNAME_KEY, lastUserName); session.setAttribute(LAST_PASSWORD_KEY, ""); if (exception != null && exception.getMessage() != null) { session.setAttribute(LAST_AUTH_FAIL_MSG, exception.getMessage()); } } } }
MyUserDetailService.java
import cn.org.pcoic.manage.entity.Account; import cn.org.pcoic.manage.service.intf.ISystemService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import javax.annotation.Resource; /** * @author Terry E-mail: yaoxinghuo at 126 dot com * @date 2021-7-7 19:34 * @description */ @Configuration("myUserDetailService") public class MyUserDetailService implements UserDetailsService { private static final Log log = LogFactory.getLog(MyUserDetailService.class); @Resource(name = "systemService") private ISystemService systemService; // 用户一登陆就会调用此方法验证,登陆界面也是spring管理的,不需要自己来写了,login.jsp里用到spring @Override public UserDetails loadUserByUsername(String no) throws UsernameNotFoundException { Account account = systemService.getAccountByNo(no); // 数据库中查询此用户 if (account == null) { // 判断此用户是否存在 throw new UsernameNotFoundException(no); } if (!account.isEnabled() || !account.isAccountNonLocked()) { throw new LockedException("账户已停用"); } log.warn(WebSecurityConfig.SECURITY_KEY + " Cookie登录,姓名:" + account.getName() + ",帐号:" + account.getNo() + ",ID:" + account.getId()); systemService.updateAccountLastLogin(account.getId(), null); return account; } }
MyWebAuthenticationDetails.java
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.Serializable; /** * @author Terry E-mail: yaoxinghuo at 126 dot com * @date 2018-3-23 09:54 * @description */ public class MyWebAuthenticationDetails implements Serializable { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final String remoteAddress; private final String sessionId; public MyWebAuthenticationDetails(HttpServletRequest request) { this.remoteAddress = Utils.getIpAddr(request); HttpSession session = request.getSession(false); this.sessionId = session != null ? session.getId() : null; } public boolean equals(Object obj) { if (obj instanceof WebAuthenticationDetails) { WebAuthenticationDetails rhs = (WebAuthenticationDetails)obj; if (this.remoteAddress == null && rhs.getRemoteAddress() != null) { return false; } else if (this.remoteAddress != null && rhs.getRemoteAddress() == null) { return false; } else if (this.remoteAddress != null && !this.remoteAddress.equals(rhs.getRemoteAddress())) { return false; } else if (this.sessionId == null && rhs.getSessionId() != null) { return false; } else if (this.sessionId != null && rhs.getSessionId() == null) { return false; } else { return this.sessionId == null || this.sessionId.equals(rhs.getSessionId()); } } else { return false; } } public String getRemoteAddress() { return this.remoteAddress; } public String getSessionId() { return this.sessionId; } public int hashCode() { int code = 7654; if (this.remoteAddress != null) { code *= this.remoteAddress.hashCode() % 7; } if (this.sessionId != null) { code *= this.sessionId.hashCode() % 7; } return code; } public String toString() { StringBuilder sb = new StringBuilder(); sb.append(super.toString()).append(": "); sb.append("RemoteIpAddress: ").append(this.getRemoteAddress()).append("; "); sb.append("SessionId: ").append(this.getSessionId()); return sb.toString(); } }
MyWebAuthenticationDetailsSource.java
import org.springframework.security.authentication.AuthenticationDetailsSource; import javax.servlet.http.HttpServletRequest; public class MyWebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, MyWebAuthenticationDetails> { @Override public MyWebAuthenticationDetails buildDetails(HttpServletRequest context) { return new MyWebAuthenticationDetails(context); } }
文章评论