本篇介绍如何在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);
}
}
文章评论